New Version

Sync with Latest Instiki Trunk.
Migrate to Rails 1.2.5.
Bump version number.
This commit is contained in:
Jacques Distler 2007-10-15 12:16:54 -05:00
parent de125367b0
commit 207fb1f7f2
120 changed files with 2592 additions and 662 deletions

View file

@ -1,9 +1,30 @@
* TRUNK: ------------------------------------------------------------------------------
* 0.12.0:
0.12 is mainly a bugfix release. We recommend all instiki Users to upgrade.
In this version, some security holes where fixed
- An XSS vulnerability in categories
- An XSS vulnerability in <nowiki>
- fixes that Instiki allows "dangerous" operations as HTTP GETs
as well as some other small improvements.
- fixes for instiki running on mongrel
- fixes for instiki running on mongrel_cluster
We added a lot of tests, synced with Jacques Distler's version and fixed
small bugs as well. A note to Mac OSX users: use the Ruby One-Click-Installer
for OSX ( http://rubyosx.com ) or make sure you are not running into problems
with sqlite (see http://instiki.5uper.net/instiki/show/SQLite+issues+on+OSX)
------------------------------------------------------------------------------
* 0.11.pl1
- ANTISPAM: - ANTISPAM:
- updated and included spam_patterns.txt - updated and included spam_patterns.txt
- included dnsbl_check - DNS Blackhole Lists check - included dnsbl_check - DNS Blackhole Lists check
[thanks to joost from http://www.spacebabies.nl ] [thanks to joost from http://www.spacebabies.nl ]
- included the form-spam-protection rails plugin
http://form-spam-protection.googlecode.com/svn/form_spam_protection/
- BUGFIXES: - BUGFIXES:
- fix PDF output not to contain garbage chars [Jesse Newland] - fix PDF output not to contain garbage chars [Jesse Newland]
@ -16,10 +37,9 @@
- lots of small bugfixes and changes - lots of small bugfixes and changes
- UPDATES: - UPDATES:
- Rails 1.2 tested and packaged with instiki - Rails 1.2.1 tested and packaged with instiki
- updated RubyZip to 0.9.1 - updated RubyZip to 0.9.1
- updated RedCloth to 3.0.4 - updated packaged sqlite3-ruby
- updated packaged sqlite3-ruby to 1.2.0
- FEATURES: - FEATURES:
- fix: being logged in on more Webs at once works now [Jaques Distler] - fix: being logged in on more Webs at once works now [Jaques Distler]

125
README
View file

@ -1,39 +1,61 @@
===What is Instiki?
Admitted, it's YetAnotherWikiClone[http://c2.com/cgi/wiki?WikiWikiClones], but with a strong focus = Instiki
on simplicity of installation and running:
Step 1. Download Instiki is a wiki clone so pretty and easy to set up, you'll wonder if its really a wiki. Runs on Rails and focuses on portability and stability. Supports file uploads, PDF export, RSS, multiple users and password protection. Some use Instiki as a CMS (Content Management System) because of it's ability to export static pages.
Instiki lowers the barriers of interest for when you might consider using a wiki. It's so simple to get running that you'll find yourself using it for anything -- taking notes, brainstorming, organizing a gathering.
== Supported Platforms
Instiki only relies on Ruby - the sole external dependency (it includes all other dependencies). Any OS that can run Ruby can run Instiki - that includes Windows, Linux, Mac OS X and most known Unix flavors.
Instiki on BeOS, Amiga OS, OS2, Zeta OS and support for various exotic Platforms is planned. Mostly it already works, if Ruby runs there (download the linux version in this case). Please contact parasew if you want to help out for your OS that is not listed. Please also get in touch if you are able to create an instiki package for your favorite unix or linux distribution.
== 3 easy Steps to get the Instiki experience
Step 1. Get Ruby, Download Instiki
Step 2. Run "instiki" Step 2. Run "instiki"
Step 3. Chuckle... "There's no step three!" (TM)
If you are on Windows:
"Step 3. Chuckle... "There's no step three!" (TM)"
You're now running a perfectly suitable wiki on port 2500 == Details
that'll present you with one-step setup, followed by a textarea for the home page
on http://localhost:2500
Instiki lowers the barriers of interest for when you might consider You need at least Ruby Version 1.8.4 installed on your System. The second dependency is a Database System, but don't worry, maybe you are already served.
using a wiki. It's so simple to get running that you'll find yourself
using it for anything -- taking notes, brainstorming, organizing a
gathering.
Having said all that, if you are not on Windows, in this version of Instiki it is a somewhat different story.
Since the author has no Linux or Mac at hand, and Instiki is moving to a SQL-based backend, this is what it takes
to install (until somebody sends a patch to properly package Instiki for all those other platforms):
3. Kill "instiki" === If you are on Windows
4. Install SQLite 3 database engine from http://www.sqlite.org/
5. Install SQLite 3 driver for Ruby from http://sqlite-ruby.rubyforge.org/ - Get the *Ruby One-Click Installer - Windows* http://rubyforge.org/projects/rubyinstaller
6. Install Rake from http://rake.rubyforge.org/ - double-click instiki.bat or instiki.cmd and there you go!
7. Execute rm -f db/*.db
8. Execute 'rake environment RAILS_ENV=production migrate' if you are running Windows 95, 98 or ME and cannot get instiki to run, try Version 0.11.pl1 which is the last instiki Version to support that old-style OS's. Please update to some Unix-OS or complain to the Ruby on Rails List at http://www.ruby-forum.com/forum/3 (Rails does not support your old Windows.)
9. Make an embarrassed sigh (as I do while writing this)
10. Run 'instiki' again
11. Pat yourself on the shoulder for being such a talented geek === If you are on Mac OSX
12. At least, there is no step twelve! (TM)
Since the Apple guys really screwed it up, having an old Ruby Version (1.8.2) and a broken Readline Library with MacOSX Tiger, you have to
- use the Ruby One-Click-Installer for OSX ( http://rubyosx.com ) if you don't already have macports' Ruby
- make sure you read http://instiki.5uper.net/instiki/show/SQLite+issues+on+OSX
- run "ruby instiki.rb" via command-line in the directory
=== If you are on Linux
=== Any other System
- get Ruby for your System, compile if nessesary: http://ruby-lang.org
- get SQLite or compile from http://sqlite.org (you can also use mysql or any other supported database system if you want)
- run instiki
You're now running a perfectly suitable wiki on port 2500 that'll present you with one-step setup, followed by a textarea for the home page on http://localhost:2500
== Features
===Features:
* Regular expression search: Find deep stuff really fast * Regular expression search: Find deep stuff really fast
* Revisions: Follow the changes on every page from birth. Rollback to an earlier rev * Revisions: Follow the changes on every page from birth. Rollback to an earlier rev
* Export to HTML or markup in a zip: Take the entire wiki with you home or for reference * Export to HTML or markup in a zip: Take the entire wiki with you home or for reference
@ -42,24 +64,27 @@ to install (until somebody sends a patch to properly package Instiki for all tho
* Password-protected webs: Keep it private * Password-protected webs: Keep it private
* Authors: Each revision is associated with an author, so you can see who changed what * Authors: Each revision is associated with an author, so you can see who changed what
* Reference tracker: Which other pages are pointing to the current? * Reference tracker: Which other pages are pointing to the current?
* Speed: Using Madelein[http://madeleine.sourceforge.net] for persistence (all pages are in memory) * Three markup choices: Textile http://www.textism.com/tools/textile
* Three markup choices: Textile[http://www.textism.com/tools/textile] (default / RedCloth http://www.whytheluckystiff.net/ruby/redcloth ),
(default / RedCloth[http://www.whytheluckystiff.net/ruby/redcloth]),
Markdown (BlueCloth[http://bluecloth.rubyforge.org]), and RDoc[http://rdoc.sourceforge.net/doc] Markdown (BlueCloth[http://bluecloth.rubyforge.org]), and RDoc[http://rdoc.sourceforge.net/doc]
* Embedded webserver: Through WEBrick[http://www.webrick.org] * Embedded webserver: Through WEBrick[http://www.webrick.org], also runs on Mongel if you want to.
* Internationalization: Wiki words in any latin, greek, cyrillian, or armenian characters * Internationalization: Wiki words in any latin, greek, cyrillian, or armenian characters
* Color diffs: Track changes through revisions * Color diffs: Track changes through revisions
* Definitely can run on SQLite and MySQL * Runs on SQLite per default, can be configured to run on PostgreSQL, MySQL, DB2, Firebird, Openbase, Oracle, SQL Server or Sybase
* May be able to run on Postgres, Oracle, DB2 and SqlServer. If you try this, and it works
(or, it doesn't, but you make it work) please write about it on Instiki.org.
== Command-line options:
===Command-line options:
* Run "ruby instiki --help" * Run "ruby instiki --help"
===History:
== History:
* See CHANGELOG * See CHANGELOG
===Migrating Instiki 0.10.2 storage to Instiki 0.11.0 database
== Migrating Instiki 0.10.2 storage to Instiki 0.11.0 database
1. Install Instiki 0.11 and check that it works (you should be able to create a web, edit and save a HomePage) 1. Install Instiki 0.11 and check that it works (you should be able to create a web, edit and save a HomePage)
2. Execute 2. Execute
ruby script\import_storage \ ruby script\import_storage \
@ -81,7 +106,9 @@ to install (until somebody sends a patch to properly package Instiki for all tho
The most common migration problem is this: if you open All Pages and see a lot of orphaned pages, The most common migration problem is this: if you open All Pages and see a lot of orphaned pages,
you forgot to run ruby script\reset_references after importing the data. you forgot to run ruby script\reset_references after importing the data.
===Upgrading from Instiki-AR Beta 1 ===Upgrading from Instiki-AR Beta 1
In Beta 2, we switch to ActiveRecord:Migrations. Therefore: In Beta 2, we switch to ActiveRecord:Migrations. Therefore:
1. Back up your production database. 1. Back up your production database.
2. Open command-line session to your database and execute: 2. Open command-line session to your database and execute:
@ -93,21 +120,33 @@ Step 2 creates a table that tells to ActiveRecord:Migrations that the current ve
of this database is 1 (corresponding to Beta 1), and step 3 makes it up-to-date with of this database is 1 (corresponding to Beta 1), and step 3 makes it up-to-date with
the current version of Instiki. the current version of Instiki.
===Download the latest release from:
== Download the latest release from:
* http://rubyforge.org/project/showfiles.php?group_id=186 * http://rubyforge.org/project/showfiles.php?group_id=186
===Visit the "official" Instiki wiki:
== Visit the "official" Instiki wiki:
* http://instiki.org * http://instiki.org
===License:
== License:
* same as Ruby's * same as Ruby's
--- ---
Authors:: Authors::
Versions 0.0 to 0.9.1:: David Heinemeier Hansson Versions 0.0 to 0.9.1:: David Heinemeier Hansson
Email:: david@loudthinking.com Email:: david[AT]loudthinking.com
Weblog:: http://www.loudthinking.com Weblog:: http://www.loudthinking.com[http://www.loudthinking.com]
From 0.9.2 onwards:: Alexey Verkhovsky From 0.9.2 onwards:: Alexey Verkhovsky
Email:: alex@verk.info Email:: alex[AT]verk.info
From 0.11 onwards:: Matthias Tarasiewicz and 5uper.net
Email:: parasew[AT]gmail.com
Website:: http://5uper.net[http://5uper.net]

View file

@ -219,7 +219,7 @@ end
module Instiki module Instiki
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 0 MAJOR = 0
MINOR = 12 MINOR = 13
TINY = 0 TINY = 0
SUFFIX = '(MML+)' SUFFIX = '(MML+)'
PRERELEASE = 'pre' # false PRERELEASE = 'pre' # false

View file

@ -25,7 +25,7 @@ class Page < ActiveRecord::Base
if (revisions_size > 0) && continous_revision?(time, author) if (revisions_size > 0) && continous_revision?(time, author)
current_revision.update_attributes(:content => content, :revised_at => time) current_revision.update_attributes(:content => content, :revised_at => time)
else else
revisions.create(:content => content, :author => author, :revised_at => time) revisions.build(:content => content, :author => author, :revised_at => time)
end end
save save
self self

2
instiki.bat Executable file
View file

@ -0,0 +1,2 @@
set PATH=.\lib\native\win32;%PATH%
ruby.exe script\server -e production

View file

@ -143,19 +143,19 @@ class PageRenderer
else else
link_type = WikiReference.link_type(@revision.page.web, referenced_name) link_type = WikiReference.link_type(@revision.page.web, referenced_name)
end end
references.create :referenced_name => referenced_name, :link_type => link_type references.build :referenced_name => referenced_name, :link_type => link_type
end end
include_chunks = rendering_result.find_chunks(Include) include_chunks = rendering_result.find_chunks(Include)
includes = include_chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq includes = include_chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq
includes.each do |included_page_name| includes.each do |included_page_name|
references.create :referenced_name => included_page_name, references.build :referenced_name => included_page_name,
:link_type => WikiReference::INCLUDED_PAGE :link_type => WikiReference::INCLUDED_PAGE
end end
categories = rendering_result.find_chunks(Category).map { |cat| cat.list }.flatten categories = rendering_result.find_chunks(Category).map { |cat| cat.list }.flatten
categories.each do |category| categories.each do |category|
references.create :referenced_name => category, :link_type => WikiReference::CATEGORY references.build :referenced_name => category, :link_type => WikiReference::CATEGORY
end end
end end
end end

View file

@ -1,5 +1,6 @@
require 'yaml' require 'yaml'
require 'time' require 'time'
require 'md5'
module ActionController module ActionController
class AbstractResponse #:nodoc: class AbstractResponse #:nodoc:

View file

@ -1,3 +1,13 @@
*1.3.5* (October 12th, 2007)
* Depend on Action Pack 1.13.5
*1.3.4* (October 4th, 2007)
* Depend on Action Pack 1.13.4
*1.3.3* (March 12th, 2007) *1.3.3* (March 12th, 2007)
* Depend on Action Pack 1.13.3 * Depend on Action Pack 1.13.3

View file

@ -54,7 +54,7 @@ spec = Gem::Specification.new do |s|
s.rubyforge_project = "actionmailer" s.rubyforge_project = "actionmailer"
s.homepage = "http://www.rubyonrails.org" s.homepage = "http://www.rubyonrails.org"
s.add_dependency('actionpack', '= 1.13.3' + PKG_BUILD) s.add_dependency('actionpack', '= 1.13.5' + PKG_BUILD)
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'

View file

@ -2,7 +2,7 @@ module ActionMailer
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 1 MAJOR = 1
MINOR = 3 MINOR = 3
TINY = 3 TINY = 5
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View file

@ -1,6 +1,55 @@
*1.13.5* (October 12th, 2007)
* Backport: allow array and hash query parameters. Array route parameters are converted/to/a/path as before. #6765, #7047, #7462 [bgipsy, Jeremy McAnally, Dan Kubb, brendan, Diego Algorta Casamayou]
* Fix in place editor's setter action with non-string fields. #7418 [Andreas]
*1.13.4* (October 4th, 2007)
* Only accept session ids from cookies, prevents session fixation attacks. [bradediger]
* Change the resource seperator from ; to / change the generated routes to use the new-style named routes. e.g. new_group_user_path(@group) instead of group_new_user_path(@group). [pixeltrix]
* Integration tests: introduce methods for other HTTP methods. #6353 [caboose]
* Improve performance of action caching. Closes #8231 [skaes]
* Fix errors with around_filters which do not yield, restore 1.1 behaviour with after filters. Closes #8891 [skaes]
After filters will *no longer* be run if an around_filter fails to yield, users relying on
this behaviour are advised to put the code in question after a yield statement in an around filter.
* Allow you to delete cookies with options. Closes #3685 [josh, Chris Wanstrath]
* Deprecate pagination. Install the classic_pagination plugin for forward compatibility, or move to the superior will_paginate plugin. #8157 [Mislav Marohnic]
* Fix filtered parameter logging with nil parameter values. #8422 [choonkeat]
* Integration tests: alias xhr to xml_http_request and add a request_method argument instead of always using POST. #7124 [Nik Wakelin, Francois Beausoleil, Wizard]
* Document caches_action. #5419 [Jarkko Laine]
* observe_form always sends the serialized form. #5271 [manfred, normelton@gmail.com]
* Update UrlWriter to accept :anchor parameter. Closes #6771. [octopod]
* Replace the current block/continuation filter chain handling by an implementation based on a simple loop. Closes #8226 [Stefan Kaes]
* Return the string representation from an Xml Builder when rendering a partial. #5044 [tpope]
* Cleaned up, corrected, and mildly expanded ActionPack documentation. Closes #7190 [jeremymcanally]
* Small collection of ActionController documentation cleanups. Closes #7319 [jeremymcanally]
* Performance: patch cgi/session/pstore to require digest/md5 once rather than per #initialize. #7583 [Stefan Kaes]
* Deprecation: verification with :redirect_to => :named_route shouldn't be deprecated. #7525 [Justin French]
*1.13.3* (March 12th, 2007) *1.13.3* (March 12th, 2007)
* Apply [5709] to stable. * Fix a bug in Routing where a parameter taken from the path of the current request could not be used as a query parameter for the next. #6752 [Nicholas Seckar]
* session_enabled? works with session :off. #6680 [Catfish] * session_enabled? works with session :off. #6680 [Catfish]
@ -440,7 +489,7 @@
* Avoid naming collision among compiled view methods. [Jeremy Kemper] * Avoid naming collision among compiled view methods. [Jeremy Kemper]
* Fix CGI extensions when they expect string but get nil in Windows. Closes #5276 [mislav@nippur.irb.hr] * Fix CGI extensions when they expect string but get nil in Windows. Closes #5276 [Mislav Marohnic]
* Determine the correct template_root for deeply nested components. #2841 [s.brink@web.de] * Determine the correct template_root for deeply nested components. #2841 [s.brink@web.de]

View file

@ -75,7 +75,7 @@ spec = Gem::Specification.new do |s|
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'
s.add_dependency('activesupport', '= 1.4.2' + PKG_BUILD) s.add_dependency('activesupport', '= 1.4.4' + PKG_BUILD)
s.require_path = 'lib' s.require_path = 'lib'
s.autorequire = 'action_controller' s.autorequire = 'action_controller'

View file

@ -1,7 +1,7 @@
module ActionController module ActionController
module Assertions module Assertions
module DomAssertions module DomAssertions
# test 2 html strings to be equivalent, i.e. identical up to reordering of attributes # Test two HTML strings for equivalency (e.g., identical up to reordering of attributes)
def assert_dom_equal(expected, actual, message="") def assert_dom_equal(expected, actual, message="")
clean_backtrace do clean_backtrace do
expected_dom = HTML::Document.new(expected).root expected_dom = HTML::Document.new(expected).root
@ -11,7 +11,7 @@ module ActionController
end end
end end
# negated form of +assert_dom_equivalent+ # The negated form of +assert_dom_equivalent+.
def assert_dom_not_equal(expected, actual, message="") def assert_dom_not_equal(expected, actual, message="")
clean_backtrace do clean_backtrace do
expected_dom = HTML::Document.new(expected).root expected_dom = HTML::Document.new(expected).root

View file

@ -1,7 +1,7 @@
module ActionController module ActionController
module Assertions module Assertions
module ModelAssertions module ModelAssertions
# ensures that the passed record is valid by active record standards. returns the error messages if not # Ensures that the passed record is valid by ActiveRecord standards and returns any error messages if it is not.
def assert_valid(record) def assert_valid(record)
clean_backtrace do clean_backtrace do
assert record.valid?, record.errors.full_messages.join("\n") assert record.valid?, record.errors.full_messages.join("\n")

View file

@ -69,6 +69,7 @@ module ActionController
end end
if value.respond_to?(:[]) && value['controller'] if value.respond_to?(:[]) && value['controller']
value['controller'] = value['controller'].to_s
if key == :actual && value['controller'].first != '/' && !value['controller'].include?('/') if key == :actual && value['controller'].first != '/' && !value['controller'].include?('/')
new_controller_path = ActionController::Routing.controller_relative_to(value['controller'], @controller.class.controller_path) new_controller_path = ActionController::Routing.controller_relative_to(value['controller'], @controller.class.controller_path)
value['controller'] = new_controller_path if value['controller'] != new_controller_path && ActionController::Routing.possible_controllers.include?(new_controller_path) value['controller'] = new_controller_path if value['controller'] != new_controller_path && ActionController::Routing.possible_controllers.include?(new_controller_path)
@ -120,6 +121,7 @@ module ActionController
end end
private private
# Recognizes the route for a given path.
def recognized_request_for(path, request_method = nil) def recognized_request_for(path, request_method = nil)
path = "/#{path}" unless path.first == '/' path = "/#{path}" unless path.first == '/'
@ -132,6 +134,7 @@ module ActionController
request request
end end
# Proxy to to_param if the object will respond to it.
def parameterize(value) def parameterize(value)
value.respond_to?(:to_param) ? value.to_param : value value.respond_to?(:to_param) ? value.to_param : value
end end

View file

@ -82,6 +82,7 @@ module ActionController
end end
private private
# Recognizes the route for a given path.
def recognized_request_for(path, request_method = nil) def recognized_request_for(path, request_method = nil)
path = "/#{path}" unless path.first == '/' path = "/#{path}" unless path.first == '/'

View file

@ -561,6 +561,8 @@ module ActionController
# RJS encodes double quotes and line breaks. # RJS encodes double quotes and line breaks.
unescaped= rjs_string.gsub('\"', '"') unescaped= rjs_string.gsub('\"', '"')
unescaped.gsub!('\n', "\n") unescaped.gsub!('\n', "\n")
unescaped.gsub!('\076', '>')
unescaped.gsub!('\074', '<')
# RJS encodes non-ascii characters. # RJS encodes non-ascii characters.
unescaped.gsub!(RJS_PATTERN_UNICODE_ESCAPED_CHAR) {|u| [$1.hex].pack('U*')} unescaped.gsub!(RJS_PATTERN_UNICODE_ESCAPED_CHAR) {|u| [$1.hex].pack('U*')}
unescaped unescaped

View file

@ -7,7 +7,6 @@ require 'action_controller/url_rewriter'
require 'action_controller/status_codes' require 'action_controller/status_codes'
require 'drb' require 'drb'
require 'set' require 'set'
require 'md5'
module ActionController #:nodoc: module ActionController #:nodoc:
class ActionControllerError < StandardError #:nodoc: class ActionControllerError < StandardError #:nodoc:
@ -293,6 +292,10 @@ module ActionController #:nodoc:
# Turn on +ignore_missing_templates+ if you want to unit test actions without making the associated templates. # Turn on +ignore_missing_templates+ if you want to unit test actions without making the associated templates.
cattr_accessor :ignore_missing_templates cattr_accessor :ignore_missing_templates
# Controls the resource action separator
@@resource_action_separator = "/"
cattr_accessor :resource_action_separator
# Holds the request object that's primarily used to get environment variables through access like # Holds the request object that's primarily used to get environment variables through access like
# <tt>request.env["REQUEST_URI"]</tt>. # <tt>request.env["REQUEST_URI"]</tt>.
attr_internal :request attr_internal :request
@ -394,7 +397,8 @@ module ActionController #:nodoc:
elsif value.is_a?(Hash) elsif value.is_a?(Hash)
filtered_parameters[key] = filter_parameters(value) filtered_parameters[key] = filter_parameters(value)
elsif block_given? elsif block_given?
key, value = key.dup, value.dup key = key.dup
value = value.dup if value
yield key, value yield key, value
filtered_parameters[key] = value filtered_parameters[key] = value
else else
@ -539,6 +543,7 @@ module ActionController #:nodoc:
self.class.controller_path self.class.controller_path
end end
# Test whether the session is enabled for this request.
def session_enabled? def session_enabled?
request.session_options && request.session_options[:disabled] != false request.session_options && request.session_options[:disabled] != false
end end
@ -600,12 +605,6 @@ module ActionController #:nodoc:
# _Deprecation_ _notice_: This used to have the signatures # _Deprecation_ _notice_: This used to have the signatures
# <tt>render_partial(partial_path = default_template_name, object = nil, local_assigns = {})</tt> and # <tt>render_partial(partial_path = default_template_name, object = nil, local_assigns = {})</tt> and
# <tt>render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = {})</tt>. # <tt>render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = {})</tt>.
# == Automatic etagging
#
# Rendering will automatically insert the etag header on 200 OK responses. The etag is calculated using MD5 of the
# response body. If a request comes in that has a matching etag, the response will be changed to a 304 Not Modified
# and the response body will be set to an empty string.
#
# #
# === Rendering a template # === Rendering a template
# #
@ -829,8 +828,6 @@ module ActionController #:nodoc:
else else
response.body = text response.body = text
end end
response.body
end end
def render_javascript(javascript, status = nil, append_response = true) #:nodoc: def render_javascript(javascript, status = nil, append_response = true) #:nodoc:

View file

@ -1,5 +1,6 @@
require 'fileutils' require 'fileutils'
require 'uri' require 'uri'
require 'set'
module ActionController #:nodoc: module ActionController #:nodoc:
# Caching is a cheap way of speeding up slow applications by keeping the result of calculations, renderings, and database calls # Caching is a cheap way of speeding up slow applications by keeping the result of calculations, renderings, and database calls
@ -163,13 +164,24 @@ module ActionController #:nodoc:
module Actions module Actions
def self.included(base) #:nodoc: def self.included(base) #:nodoc:
base.extend(ClassMethods) base.extend(ClassMethods)
base.send(:attr_accessor, :rendered_action_cache) base.class_eval do
attr_accessor :rendered_action_cache, :action_cache_path
alias_method_chain :protected_instance_variables, :action_caching
end
end end
module ClassMethods #:nodoc: def protected_instance_variables_with_action_caching
protected_instance_variables_without_action_caching + %w(@action_cache_path)
end
module ClassMethods
# Declares that +actions+ should be cached.
# See ActionController::Caching::Actions for details.
def caches_action(*actions) def caches_action(*actions)
return unless perform_caching return unless perform_caching
around_filter(ActionCacheFilter.new(*actions)) action_cache_filter = ActionCacheFilter.new(*actions)
before_filter action_cache_filter
after_filter action_cache_filter
end end
end end
@ -185,70 +197,59 @@ module ActionController #:nodoc:
end end
class ActionCacheFilter #:nodoc: class ActionCacheFilter #:nodoc:
def initialize(*actions, &block) def initialize(*actions)
@actions = actions @actions = Set.new actions
end end
def before(controller) def before(controller)
return unless @actions.include?(controller.action_name.intern) return unless @actions.include?(controller.action_name.to_sym)
action_cache_path = ActionCachePath.new(controller) cache_path = ActionCachePath.new(controller, {})
if cache = controller.read_fragment(action_cache_path.path) if cache = controller.read_fragment(cache_path.path)
controller.rendered_action_cache = true controller.rendered_action_cache = true
set_content_type!(action_cache_path) set_content_type!(controller, cache_path.extension)
controller.send(:render_text, cache) controller.send(:render_text, cache)
false false
else
controller.action_cache_path = cache_path
end end
end end
def after(controller) def after(controller)
return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache return if !@actions.include?(controller.action_name.to_sym) || controller.rendered_action_cache
controller.write_fragment(ActionCachePath.path_for(controller), controller.response.body) controller.write_fragment(controller.action_cache_path.path, controller.response.body)
end end
private private
def set_content_type!(controller, extension)
def set_content_type!(action_cache_path) controller.response.content_type = Mime::EXTENSION_LOOKUP[extension].to_s if extension
if extention = action_cache_path.extension
content_type = Mime::EXTENSION_LOOKUP[extention]
action_cache_path.controller.response.content_type = content_type.to_s
end
end end
end end
class ActionCachePath class ActionCachePath
attr_reader :controller, :options attr_reader :path, :extension
class << self class << self
def path_for(*args, &block) def path_for(controller, options)
new(*args).path new(controller, options).path
end end
end end
def initialize(controller, options = {}) def initialize(controller, options = {})
@controller = controller @extension = extract_extension(controller.request.path)
@options = options path = controller.url_for(options).split('://').last
end normalize!(path)
add_extension!(path, @extension)
def path @path = URI.unescape(path)
return @path if @path
@path = controller.url_for(options).split('://').last
normalize!
add_extension!
URI.unescape(@path)
end
def extension
@extension ||= extract_extension(controller.request.path)
end end
private private
def normalize! def normalize!(path)
@path << 'index' if @path.last == '/' path << 'index' if path[-1] == ?/
end end
def add_extension! def add_extension!(path, extension)
@path << ".#{extension}" if extension path << ".#{extension}" if extension
end end
def extract_extension(file_path) def extract_extension(file_path)
@ -472,7 +473,6 @@ module ActionController #:nodoc:
end end
def write(name, value, options = nil) #:nodoc: def write(name, value, options = nil) #:nodoc:
File.umask(0006)
ensure_cache_path(File.dirname(real_file_path(name))) ensure_cache_path(File.dirname(real_file_path(name)))
File.open(real_file_path(name), "wb+") { |f| f.write(value) } File.open(real_file_path(name), "wb+") { |f| f.write(value) }
rescue => e rescue => e

View file

@ -0,0 +1,30 @@
# CGI::Session::PStore.initialize requires 'digest/md5' on every call.
# This makes sense when spawning processes per request, but is
# unnecessarily expensive when serving requests from a long-lived
# process.
require 'cgi/session'
require 'cgi/session/pstore'
require 'digest/md5'
class CGI::Session::PStore #:nodoc:
def initialize(session, option={})
dir = option['tmpdir'] || Dir::tmpdir
prefix = option['prefix'] || ''
id = session.session_id
md5 = Digest::MD5.hexdigest(id)[0,16]
path = dir+"/"+prefix+md5
path.untaint
if File::exist?(path)
@hash = nil
else
unless session.new_session
raise CGI::Session::NoSession, "uninitialized session"
end
@hash = {}
end
@p = ::PStore.new(path)
@p.transaction do |p|
File.chmod(0600, p.path)
end
end
end

View file

@ -65,7 +65,7 @@ class CGI #:nodoc:
if env_qs.blank? && !(uri = env_table['REQUEST_URI']).blank? if env_qs.blank? && !(uri = env_table['REQUEST_URI']).blank?
uri.split('?', 2)[1] || '' uri.split('?', 2)[1] || ''
else else
env_qs env_qs || ''
end end
end end
end end

View file

@ -2,6 +2,7 @@ require 'action_controller/cgi_ext/cgi_ext'
require 'action_controller/cgi_ext/cookie_performance_fix' require 'action_controller/cgi_ext/cookie_performance_fix'
require 'action_controller/cgi_ext/raw_post_data_fix' require 'action_controller/cgi_ext/raw_post_data_fix'
require 'action_controller/cgi_ext/session_performance_fix' require 'action_controller/cgi_ext/session_performance_fix'
require 'action_controller/cgi_ext/pstore_performance_fix'
module ActionController #:nodoc: module ActionController #:nodoc:
class Base class Base
@ -12,8 +13,8 @@ module ActionController #:nodoc:
# (default). Additionally, there is CGI::Session::DRbStore and CGI::Session::ActiveRecordStore. Read more about these in # (default). Additionally, there is CGI::Session::DRbStore and CGI::Session::ActiveRecordStore. Read more about these in
# lib/action_controller/session. # lib/action_controller/session.
# * <tt>:session_key</tt> - the parameter name used for the session id. Defaults to '_session_id'. # * <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 # * <tt>:session_id</tt> - the session id to use. If not provided, then it is retrieved from the +session_key+ cookie, or
# of the request, or automatically generated for a new session. # 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 # * <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, # 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. # an ArgumentError is raised.
@ -23,6 +24,8 @@ module ActionController #:nodoc:
# server. # server.
# * <tt>:session_secure</tt> - if +true+, this session will only work over HTTPS. # * <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. # * <tt>:session_path</tt> - the path for which this session applies. Defaults to the directory of the CGI script.
# * <tt>:cookie_only</tt> - if +true+ (the default), session IDs will only be accepted from cookies and not from
# the query string or POST parameters. This protects against session fixation attacks.
def self.process_cgi(cgi = CGI.new, session_options = {}) def self.process_cgi(cgi = CGI.new, session_options = {})
new.process_cgi(cgi, session_options) new.process_cgi(cgi, session_options)
end end
@ -33,18 +36,21 @@ module ActionController #:nodoc:
end end
class CgiRequest < AbstractRequest #:nodoc: class CgiRequest < AbstractRequest #:nodoc:
attr_accessor :cgi, :session_options attr_accessor :cgi, :session_options, :cookie_only
class SessionFixationAttempt < StandardError; end #:nodoc:
DEFAULT_SESSION_OPTIONS = { DEFAULT_SESSION_OPTIONS = {
:database_manager => CGI::Session::PStore, :database_manager => CGI::Session::PStore,
:prefix => "ruby_sess.", :prefix => "ruby_sess.",
:session_path => "/" :session_path => "/",
:cookie_only => true
} unless const_defined?(:DEFAULT_SESSION_OPTIONS) } unless const_defined?(:DEFAULT_SESSION_OPTIONS)
def initialize(cgi, session_options = {}) def initialize(cgi, session_options = {})
@cgi = cgi @cgi = cgi
@session_options = session_options @session_options = session_options
@env = @cgi.send(:env_table) @env = @cgi.send(:env_table)
@cookie_only = session_options.delete :cookie_only
super() super()
end end
@ -108,6 +114,9 @@ module ActionController #:nodoc:
@session = Hash.new @session = Hash.new
else else
stale_session_check! do stale_session_check! do
if @cookie_only && request_parameters[session_options_with_string_keys['session_key']]
raise SessionFixationAttempt
end
case value = session_options_with_string_keys['new_session'] case value = session_options_with_string_keys['new_session']
when true when true
@session = new_session @session = new_session

View file

@ -62,9 +62,11 @@ module ActionController #:nodoc:
end end
# Removes the cookie on the client machine by setting the value to an empty string # Removes the cookie on the client machine by setting the value to an empty string
# and setting its expiration date into the past # and setting its expiration date into the past. Like []=, you can pass in an options
def delete(name) # hash to delete cookies with extra data such as a +path+.
set_cookie("name" => name.to_s, "value" => "", "expires" => Time.at(0)) def delete(name, options = {})
options.stringify_keys!
set_cookie(options.merge("name" => name.to_s, "value" => "", "expires" => Time.at(0)))
end end
private private

View file

@ -214,9 +214,10 @@ module ActionController #:nodoc:
# == Filter Chain Halting # == Filter Chain Halting
# #
# <tt>before_filter</tt> and <tt>around_filter</tt> may halt the request # <tt>before_filter</tt> and <tt>around_filter</tt> may halt the request
# before controller action is run. This is useful, for example, to deny # before a controller action is run. This is useful, for example, to deny
# access to unauthenticated users or to redirect from http to https. # access to unauthenticated users or to redirect from http to https.
# Simply return false from the filter or call render or redirect. # Simply return false from the filter or call render or redirect.
# After filters will not be executed if the filter chain is halted.
# #
# Around filters halt the request unless the action block is called. # Around filters halt the request unless the action block is called.
# Given these filters # Given these filters
@ -238,12 +239,12 @@ module ActionController #:nodoc:
# . . / # . . /
# . #around (code after yield) # . #around (code after yield)
# . / # . /
# #after (actual filter code is run) # #after (actual filter code is run, unless the around filter does not yield)
# #
# If #around returns before yielding, only #after will be run. The #before # If #around returns before yielding, #after will still not be run. The #before
# filter and controller action will not be run. If #before returns false, # filter and controller action will not be run. If #before returns false,
# the second half of #around and all of #after will still run but the # the second half of #around and will still run but #after and the
# action will not. # action will not. If #around does not yield, #after will not be run.
module ClassMethods module ClassMethods
# The passed <tt>filters</tt> will be appended to the filter_chain and # The passed <tt>filters</tt> will be appended to the filter_chain and
# will execute before the action on this controller is performed. # will execute before the action on this controller is performed.
@ -263,13 +264,13 @@ module ActionController #:nodoc:
# The passed <tt>filters</tt> will be appended to the array of filters # The passed <tt>filters</tt> will be appended to the array of filters
# that run _after_ actions on this controller are performed. # that run _after_ actions on this controller are performed.
def append_after_filter(*filters, &block) def append_after_filter(*filters, &block)
prepend_filter_to_chain(filters, :after, &block) append_filter_to_chain(filters, :after, &block)
end end
# The passed <tt>filters</tt> will be prepended to the array of filters # The passed <tt>filters</tt> will be prepended to the array of filters
# that run _after_ actions on this controller are performed. # that run _after_ actions on this controller are performed.
def prepend_after_filter(*filters, &block) def prepend_after_filter(*filters, &block)
append_filter_to_chain(filters, :after, &block) prepend_filter_to_chain(filters, :after, &block)
end end
# Shorthand for append_after_filter since it's the most common. # Shorthand for append_after_filter since it's the most common.
@ -362,12 +363,12 @@ module ActionController #:nodoc:
# Returns a mapping between filters and the actions that may run them. # Returns a mapping between filters and the actions that may run them.
def included_actions #:nodoc: def included_actions #:nodoc:
read_inheritable_attribute("included_actions") || {} @included_actions ||= read_inheritable_attribute("included_actions") || {}
end end
# Returns a mapping between filters and actions that may not run them. # Returns a mapping between filters and actions that may not run them.
def excluded_actions #:nodoc: def excluded_actions #:nodoc:
read_inheritable_attribute("excluded_actions") || {} @excluded_actions ||= read_inheritable_attribute("excluded_actions") || {}
end end
# Find a filter in the filter_chain where the filter method matches the _filter_ param # Find a filter in the filter_chain where the filter method matches the _filter_ param
@ -381,10 +382,11 @@ module ActionController #:nodoc:
# Returns true if the filter is excluded from the given action # Returns true if the filter is excluded from the given action
def filter_excluded_from_action?(filter,action) #:nodoc: def filter_excluded_from_action?(filter,action) #:nodoc:
if (ia = included_actions[filter]) && !ia.empty? case
when ia = included_actions[filter]
!ia.include?(action) !ia.include?(action)
else when ea = excluded_actions[filter]
(excluded_actions[filter] || []).include?(action) ea.include?(action)
end end
end end
@ -397,20 +399,28 @@ module ActionController #:nodoc:
@filter = filter @filter = filter
end end
def type
:around
end
def before? def before?
false type == :before
end end
def after? def after?
false type == :after
end end
def around? def around?
true type == :around
end
def run(controller)
raise ActionControllerError, 'No filter type: Nothing to do here.'
end end
def call(controller, &block) def call(controller, &block)
raise(ActionControllerError, 'No filter type: Nothing to do here.') run(controller)
end end
end end
@ -420,35 +430,38 @@ module ActionController #:nodoc:
def filter def filter
@filter.filter @filter.filter
end end
def around?
false
end
end end
class BeforeFilterProxy < FilterProxy #:nodoc: class BeforeFilterProxy < FilterProxy #:nodoc:
def before? def type
true :before
end end
def call(controller, &block) def run(controller)
if false == @filter.call(controller) # must only stop if equal to false. only filters returning false are halted. # only filters returning false are halted.
controller.halt_filter_chain(@filter, :returned_false) if false == @filter.call(controller)
else controller.send :halt_filter_chain, @filter, :returned_false
yield
end end
end end
def call(controller)
yield unless run(controller)
end
end end
class AfterFilterProxy < FilterProxy #:nodoc: class AfterFilterProxy < FilterProxy #:nodoc:
def after? def type
true :after
end end
def call(controller, &block) def run(controller)
yield
@filter.call(controller) @filter.call(controller)
end end
def call(controller)
yield
run(controller)
end
end end
class SymbolFilter < Filter #:nodoc: class SymbolFilter < Filter #:nodoc:
@ -485,29 +498,72 @@ module ActionController #:nodoc:
end end
end end
class ClassBeforeFilter < Filter #:nodoc:
def call(controller, &block)
@filter.before(controller)
end
end
class ClassAfterFilter < Filter #:nodoc:
def call(controller, &block)
@filter.after(controller)
end
end
protected protected
def append_filter_to_chain(filters, position = :around, &block) def append_filter_to_chain(filters, filter_type = :around, &block)
write_inheritable_array('filter_chain', create_filters(filters, position, &block) ) pos = find_filter_append_position(filters, filter_type)
update_filter_chain(filters, filter_type, pos, &block)
end end
def prepend_filter_to_chain(filters, position = :around, &block) def prepend_filter_to_chain(filters, filter_type = :around, &block)
write_inheritable_attribute('filter_chain', create_filters(filters, position, &block) + filter_chain) pos = find_filter_prepend_position(filters, filter_type)
update_filter_chain(filters, filter_type, pos, &block)
end end
def create_filters(filters, position, &block) #:nodoc: def update_filter_chain(filters, filter_type, pos, &block)
new_filters = create_filters(filters, filter_type, &block)
new_chain = filter_chain.insert(pos, new_filters).flatten
write_inheritable_attribute('filter_chain', new_chain)
end
def find_filter_append_position(filters, filter_type)
# appending an after filter puts it at the end of the call chain
# before and around filters go before the first after filter in the chain
unless filter_type == :after
filter_chain.each_with_index do |f,i|
return i if f.after?
end
end
return -1
end
def find_filter_prepend_position(filters, filter_type)
# prepending a before or around filter puts it at the front of the call chain
# after filters go before the first after filter in the chain
if filter_type == :after
filter_chain.each_with_index do |f,i|
return i if f.after?
end
return -1
end
return 0
end
def create_filters(filters, filter_type, &block) #:nodoc:
filters, conditions = extract_conditions(filters, &block) filters, conditions = extract_conditions(filters, &block)
filters.map! { |filter| find_or_create_filter(filter,position) } filters.map! { |filter| find_or_create_filter(filter, filter_type) }
update_conditions(filters, conditions) update_conditions(filters, conditions)
filters filters
end end
def find_or_create_filter(filter,position) def find_or_create_filter(filter, filter_type)
if found_filter = find_filter(filter) { |f| f.send("#{position}?") } if found_filter = find_filter(filter) { |f| f.type == filter_type }
found_filter found_filter
else else
f = class_for_filter(filter).new(filter) f = class_for_filter(filter, filter_type).new(filter)
# apply proxy to filter if necessary # apply proxy to filter if necessary
case position case filter_type
when :before when :before
BeforeFilterProxy.new(f) BeforeFilterProxy.new(f)
when :after when :after
@ -520,7 +576,7 @@ module ActionController #:nodoc:
# The determination of the filter type was once done at run time. # The determination of the filter type was once done at run time.
# This method is here to extract as much logic from the filter run time as possible # This method is here to extract as much logic from the filter run time as possible
def class_for_filter(filter) #:nodoc: def class_for_filter(filter, filter_type) #:nodoc:
case case
when filter.is_a?(Symbol) when filter.is_a?(Symbol)
SymbolFilter SymbolFilter
@ -534,8 +590,12 @@ module ActionController #:nodoc:
end end
when filter.respond_to?(:filter) when filter.respond_to?(:filter)
ClassFilter ClassFilter
when filter.respond_to?(:before) && filter_type == :before
ClassBeforeFilter
when filter.respond_to?(:after) && filter_type == :after
ClassAfterFilter
else else
raise(ActionControllerError, 'A filters must be a Symbol, Proc, Method, or object responding to filter.') raise(ActionControllerError, 'A filter must be a Symbol, Proc, Method, or object responding to filter, after or before.')
end end
end end
@ -550,8 +610,8 @@ module ActionController #:nodoc:
return if conditions.empty? return if conditions.empty?
if conditions[:only] if conditions[:only]
write_inheritable_hash('included_actions', condition_hash(filters, conditions[:only])) write_inheritable_hash('included_actions', condition_hash(filters, conditions[:only]))
else elsif conditions[:except]
write_inheritable_hash('excluded_actions', condition_hash(filters, conditions[:except])) if conditions[:except] write_inheritable_hash('excluded_actions', condition_hash(filters, conditions[:except]))
end end
end end
@ -576,9 +636,9 @@ module ActionController #:nodoc:
def remove_actions_from_included_actions!(filters,*actions) def remove_actions_from_included_actions!(filters,*actions)
actions = actions.flatten.map(&:to_s) actions = actions.flatten.map(&:to_s)
updated_hash = filters.inject(included_actions) do |hash,filter| updated_hash = filters.inject(read_inheritable_attribute('included_actions')||{}) do |hash,filter|
ia = (hash[filter] || []) - actions ia = (hash[filter] || []) - actions
ia.blank? ? hash.delete(filter) : hash[filter] = ia ia.empty? ? hash.delete(filter) : hash[filter] = ia
hash hash
end end
write_inheritable_attribute('included_actions', updated_hash) write_inheritable_attribute('included_actions', updated_hash)
@ -595,7 +655,9 @@ module ActionController #:nodoc:
def proxy_before_and_after_filter(filter) #:nodoc: def proxy_before_and_after_filter(filter) #:nodoc:
return filter unless filter_responds_to_before_and_after(filter) return filter unless filter_responds_to_before_and_after(filter)
Proc.new do |controller, action| Proc.new do |controller, action|
unless filter.before(controller) == false if filter.before(controller) == false
controller.send :halt_filter_chain, filter, :returned_false
else
begin begin
action.call action.call
ensure ensure
@ -615,46 +677,83 @@ module ActionController #:nodoc:
end end
end end
def perform_action_with_filters protected
call_filter(self.class.filter_chain, 0)
end
def process_with_filters(request, response, method = :perform_action, *arguments) #:nodoc: def process_with_filters(request, response, method = :perform_action, *arguments) #:nodoc:
@before_filter_chain_aborted = false @before_filter_chain_aborted = false
process_without_filters(request, response, method, *arguments) process_without_filters(request, response, method, *arguments)
end end
def filter_chain def perform_action_with_filters
self.class.filter_chain call_filters(self.class.filter_chain, 0, 0)
end
def call_filter(chain, index)
return (performed? || perform_action_without_filters) if index >= chain.size
filter = chain[index]
return call_filter(chain, index.next) if self.class.filter_excluded_from_action?(filter,action_name)
halted = false
filter.call(self) do
halted = call_filter(chain, index.next)
end
halt_filter_chain(filter.filter, :no_yield) if halted == false unless @before_filter_chain_aborted
halted
end
def halt_filter_chain(filter, reason)
if logger
case reason
when :no_yield
logger.info "Filter chain halted as [#{filter.inspect}] did not yield."
when :returned_false
logger.info "Filter chain halted as [#{filter.inspect}] returned false."
end
end
@before_filter_chain_aborted = true
return false
end end
private private
def call_filters(chain, index, nesting)
index = run_before_filters(chain, index, nesting)
aborted = @before_filter_chain_aborted
perform_action_without_filters unless performed? || aborted
return index if nesting != 0 || aborted
run_after_filters(chain, index)
end
def skip_excluded_filters(chain, index)
while (filter = chain[index]) && self.class.filter_excluded_from_action?(filter, action_name)
index = index.next
end
[filter, index]
end
def run_before_filters(chain, index, nesting)
while chain[index]
filter, index = skip_excluded_filters(chain, index)
break unless filter # end of call chain reached
case filter.type
when :before
filter.run(self) # invoke before filter
index = index.next
break if @before_filter_chain_aborted
when :around
yielded = false
filter.call(self) do
yielded = true
# all remaining before and around filters will be run in this call
index = call_filters(chain, index.next, nesting.next)
end
halt_filter_chain(filter, :did_not_yield) unless yielded
break
else
break # no before or around filters left
end
end
index
end
def run_after_filters(chain, index)
seen_after_filter = false
while chain[index]
filter, index = skip_excluded_filters(chain, index)
break unless filter # end of call chain reached
case filter.type
when :after
seen_after_filter = true
filter.run(self) # invoke after filter
else
# implementation error or someone has mucked with the filter chain
raise ActionControllerError, "filter #{filter.inspect} was in the wrong place!" if seen_after_filter
end
index = index.next
end
index.next
end
def halt_filter_chain(filter, reason)
@before_filter_chain_aborted = true
logger.info "Filter chain halted as [#{filter.inspect}] #{reason}." if logger
false
end
def process_cleanup_with_filters def process_cleanup_with_filters
if @before_filter_chain_aborted if @before_filter_chain_aborted
close_session close_session

View file

@ -172,21 +172,31 @@ module ActionController
process :head, path, parameters, headers process :head, path, parameters, headers
end end
# Performs an XMLHttpRequest request with the given parameters, mimicing # Performs an XMLHttpRequest request with the given parameters, mirroring
# the request environment created by the Prototype library. The parameters # a request from the Prototype library.
# may be +nil+, a Hash, or a string that is appropriately encoded #
# (application/x-www-form-urlencoded or multipart/form-data). The headers # The request_method is :get, :post, :put, :delete or :head; the
# should be a hash. The keys will automatically be upcased, with the # parameters are +nil+, a hash, or a url-encoded or multipart string;
# prefix 'HTTP_' added if needed. # the headers are a hash. Keys are automatically upcased and prefixed
def xml_http_request(path, parameters=nil, headers=nil) # with 'HTTP_' if not already.
headers = (headers || {}).merge( #
"X-Requested-With" => "XMLHttpRequest", # This method used to omit the request_method parameter, assuming it
"Accept" => "text/javascript, text/html, application/xml, text/xml, */*" # was :post. This was deprecated in Rails 1.2.4. Always pass the request
) # method as the first argument.
def xml_http_request(request_method, path, parameters = nil, headers = nil)
post(path, parameters, headers) unless request_method.is_a?(Symbol)
ActiveSupport::Deprecation.warn 'xml_http_request now takes the request_method (:get, :post, etc.) as the first argument. It used to assume :post, so add the :post argument to your existing method calls to silence this warning.'
request_method, path, parameters, headers = :post, request_method, path, parameters
end end
headers ||= {}
headers['X-Requested-With'] = 'XMLHttpRequest'
headers['Accept'] = 'text/javascript, text/html, application/xml, text/xml, */*'
process(request_method, path, parameters, headers)
end
alias xhr :xml_http_request
# Returns the URL for the given options, according to the rules specified # Returns the URL for the given options, according to the rules specified
# in the application's routes. # in the application's routes.
def url_for(options) def url_for(options)
@ -490,7 +500,7 @@ module ActionController
@integration_session = open_session @integration_session = open_session
end end
%w(get post cookies assigns xml_http_request).each do |method| %w(get post put head delete cookies assigns xml_http_request).each do |method|
define_method(method) do |*args| define_method(method) do |*args|
reset! unless @integration_session reset! unless @integration_session
# reset the html_document variable, but only for new get/post calls # reset the html_document variable, but only for new get/post calls

View file

@ -24,7 +24,7 @@ module ActionController
define_method("set_#{object}_#{attribute}") do define_method("set_#{object}_#{attribute}") do
@item = object.to_s.camelize.constantize.find(params[:id]) @item = object.to_s.camelize.constantize.find(params[:id])
@item.update_attribute(attribute, params[:value]) @item.update_attribute(attribute, params[:value])
render :text => @item.send(attribute) render :text => @item.send(attribute).to_s
end end
end end
end end

View file

@ -1,7 +1,9 @@
module ActionController module ActionController
# === Action Pack pagination for Active Record collections # === Action Pack pagination for Active Record collections
# #
# DEPRECATION WARNING: Pagination will be separated into its own plugin with Rails 2.0. # DEPRECATION WARNING: Pagination will be moved to a plugin in Rails 2.0.
# Install the classic_pagination plugin for forward compatibility:
# script/plugin install svn://errtheblog.com/svn/plugins/classic_pagination
# #
# The Pagination module aids in the process of paging large collections of # The Pagination module aids in the process of paging large collections of
# Active Record objects. It offers macro-style automatic fetching of your # Active Record objects. It offers macro-style automatic fetching of your
@ -130,6 +132,8 @@ module ActionController
paginator_and_collection_for(collection_id, options) paginator_and_collection_for(collection_id, options)
end end
deprecate :paginate => 'Pagination is moving to a plugin in Rails 2.0: script/plugin install svn://errtheblog.com/svn/plugins/classic_pagination'
# These methods become class methods on any controller # These methods become class methods on any controller
module ClassMethods module ClassMethods
# Creates a +before_filter+ which automatically paginates an Active # Creates a +before_filter+ which automatically paginates an Active
@ -148,6 +152,8 @@ module ActionController
OPTIONS[self][collection_id] = options OPTIONS[self][collection_id] = options
end end
end end
deprecate :paginate => 'Pagination is moving to a plugin in Rails 2.0: script/plugin install svn://errtheblog.com/svn/plugins/classic_pagination'
end end
def create_paginators_and_retrieve_collections #:nodoc: def create_paginators_and_retrieve_collections #:nodoc:

View file

@ -50,10 +50,6 @@ module ActionController
@env['REQUEST_METHOD'].downcase.to_sym == :head @env['REQUEST_METHOD'].downcase.to_sym == :head
end end
def headers
@env
end
# Determine whether the body of a HTTP call is URL-encoded (default) # Determine whether the body of a HTTP call is URL-encoded (default)
# or matches one of the registered param_parsers. # or matches one of the registered param_parsers.
# #

View file

@ -2,7 +2,7 @@ module ActionController
module Resources module Resources
class Resource #:nodoc: class Resource #:nodoc:
attr_reader :collection_methods, :member_methods, :new_methods attr_reader :collection_methods, :member_methods, :new_methods
attr_reader :path_prefix, :name_prefix attr_reader :path_prefix, :new_name_prefix
attr_reader :plural, :singular attr_reader :plural, :singular
attr_reader :options attr_reader :options
@ -37,6 +37,26 @@ module ActionController
@nesting_path_prefix ||= "#{path}/:#{singular}_id" @nesting_path_prefix ||= "#{path}/:#{singular}_id"
end end
def deprecate_name_prefix?
@name_prefix.blank? && !@new_name_prefix.blank?
end
def name_prefix
deprecate_name_prefix? ? @new_name_prefix : @name_prefix
end
def old_name_prefix
@name_prefix
end
def nesting_name_prefix
"#{new_name_prefix}#{singular}_"
end
def action_separator
@action_separator ||= Base.resource_action_separator
end
protected protected
def arrange_actions def arrange_actions
@collection_methods = arrange_actions_by_methods(options.delete(:collection)) @collection_methods = arrange_actions_by_methods(options.delete(:collection))
@ -52,6 +72,7 @@ module ActionController
def set_prefixes def set_prefixes
@path_prefix = options.delete(:path_prefix) @path_prefix = options.delete(:path_prefix)
@name_prefix = options.delete(:name_prefix) @name_prefix = options.delete(:name_prefix)
@new_name_prefix = options.delete(:new_name_prefix)
end end
def arrange_actions_by_methods(actions) def arrange_actions_by_methods(actions)
@ -178,11 +199,11 @@ module ActionController
# #
# The comment resources work the same, but must now include a value for :article_id. # The comment resources work the same, but must now include a value for :article_id.
# #
# comments_url(@article) # article_comments_url(@article)
# comment_url(@article, @comment) # article_comment_url(@article, @comment)
# #
# comments_url(:article_id => @article) # article_comments_url(:article_id => @article)
# comment_url(:article_id => @article, :id => @comment) # article_comment_url(:article_id => @article, :id => @comment)
# #
# * <tt>:name_prefix</tt> -- define a prefix for all generated routes, usually ending in an underscore. # * <tt>:name_prefix</tt> -- define a prefix for all generated routes, usually ending in an underscore.
# Use this if you have named routes that may clash. # Use this if you have named routes that may clash.
@ -192,7 +213,7 @@ module ActionController
# #
# * <tt>:collection</tt> -- add named routes for other actions that operate on the collection. # * <tt>:collection</tt> -- add named routes for other actions that operate on the collection.
# Takes a hash of <tt>#{action} => #{method}</tt>, where method is <tt>:get</tt>/<tt>:post</tt>/<tt>:put</tt>/<tt>:delete</tt> # Takes a hash of <tt>#{action} => #{method}</tt>, where method is <tt>:get</tt>/<tt>:post</tt>/<tt>:put</tt>/<tt>:delete</tt>
# or <tt>:any</tt> if the method does not matter. These routes map to a URL like /messages;rss, with a route of rss_messages_url. # or <tt>:any</tt> if the method does not matter. These routes map to a URL like /messages/rss, with a route of rss_messages_url.
# * <tt>:member</tt> -- same as :collection, but for actions that operate on a specific member. # * <tt>:member</tt> -- same as :collection, but for actions that operate on a specific member.
# * <tt>:new</tt> -- same as :collection, but for actions that operate on the new resource action. # * <tt>:new</tt> -- same as :collection, but for actions that operate on the new resource action.
# #
@ -204,19 +225,19 @@ module ActionController
# # --> GET /thread/7/messages/1 # # --> GET /thread/7/messages/1
# #
# map.resources :messages, :collection => { :rss => :get } # map.resources :messages, :collection => { :rss => :get }
# # --> GET /messages;rss (maps to the #rss action) # # --> GET /messages/rss (maps to the #rss action)
# # also adds a named route called "rss_messages" # # also adds a named route called "rss_messages"
# #
# map.resources :messages, :member => { :mark => :post } # map.resources :messages, :member => { :mark => :post }
# # --> POST /messages/1;mark (maps to the #mark action) # # --> POST /messages/1/mark (maps to the #mark action)
# # also adds a named route called "mark_message" # # also adds a named route called "mark_message"
# #
# map.resources :messages, :new => { :preview => :post } # map.resources :messages, :new => { :preview => :post }
# # --> POST /messages/new;preview (maps to the #preview action) # # --> POST /messages/new/preview (maps to the #preview action)
# # also adds a named route called "preview_new_message" # # also adds a named route called "preview_new_message"
# #
# map.resources :messages, :new => { :new => :any, :preview => :post } # map.resources :messages, :new => { :new => :any, :preview => :post }
# # --> POST /messages/new;preview (maps to the #preview action) # # --> POST /messages/new/preview (maps to the #preview action)
# # also adds a named route called "preview_new_message" # # also adds a named route called "preview_new_message"
# # --> /messages/new can be invoked via any request method # # --> /messages/new can be invoked via any request method
# #
@ -235,9 +256,10 @@ module ActionController
# /account profile. # /account profile.
# #
# See map.resources for general conventions. These are the main differences: # See map.resources for general conventions. These are the main differences:
# - a singular name is given to map.resource. The default controller name is taken from the singular name. # - A singular name is given to map.resource. The default controller name is taken from the singular name.
# - To specify a custom plural name, use the :plural option. There is no :singular option # - There is no <tt>:collection</tt> option as there is only the singleton resource.
# - No default index, new, or create routes are created for the singleton resource controller. # - There is no <tt>:singular</tt> option as the singular name is passed to map.resource.
# - No default index route is created for the singleton resource controller.
# - When nesting singleton resources, only the singular name is used as the path prefix (example: 'account/messages/1') # - When nesting singleton resources, only the singular name is used as the path prefix (example: 'account/messages/1')
# #
# Example: # Example:
@ -300,7 +322,7 @@ module ActionController
map_member_actions(map, resource) map_member_actions(map, resource)
if block_given? if block_given?
with_options(:path_prefix => resource.nesting_path_prefix, &block) with_options(:path_prefix => resource.nesting_path_prefix, :new_name_prefix => resource.nesting_name_prefix, &block)
end end
end end
end end
@ -315,7 +337,7 @@ module ActionController
map_member_actions(map, resource) map_member_actions(map, resource)
if block_given? if block_given?
with_options(:path_prefix => resource.nesting_path_prefix, &block) with_options(:path_prefix => resource.nesting_path_prefix, :new_name_prefix => resource.nesting_name_prefix, &block)
end end
end end
end end
@ -324,8 +346,21 @@ module ActionController
resource.collection_methods.each do |method, actions| resource.collection_methods.each do |method, actions|
actions.each do |action| actions.each do |action|
action_options = action_options_for(action, resource, method) action_options = action_options_for(action, resource, method)
map.named_route("#{resource.name_prefix}#{action}_#{resource.plural}", "#{resource.path};#{action}", action_options)
map.named_route("formatted_#{resource.name_prefix}#{action}_#{resource.plural}", "#{resource.path}.:format;#{action}", action_options) unless resource.old_name_prefix.blank?
map.deprecated_named_route("#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.old_name_prefix}#{action}_#{resource.plural}")
map.deprecated_named_route("formatted_#{action}_#{resource.name_prefix}#{resource.plural}", "formatted_#{resource.old_name_prefix}#{action}_#{resource.plural}")
end
if resource.deprecate_name_prefix?
map.deprecated_named_route("#{action}_#{resource.name_prefix}#{resource.plural}", "#{action}_#{resource.plural}")
map.deprecated_named_route("formatted_#{action}_#{resource.name_prefix}#{resource.plural}", "formatted_#{action}_#{resource.plural}")
end
map.named_route("#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}", action_options)
map.connect("#{resource.path};#{action}", action_options)
map.connect("#{resource.path}.:format;#{action}", action_options)
map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}.:format", action_options)
end end
end end
end end
@ -335,6 +370,11 @@ module ActionController
map.named_route("#{resource.name_prefix}#{resource.plural}", resource.path, index_action_options) map.named_route("#{resource.name_prefix}#{resource.plural}", resource.path, index_action_options)
map.named_route("formatted_#{resource.name_prefix}#{resource.plural}", "#{resource.path}.:format", index_action_options) map.named_route("formatted_#{resource.name_prefix}#{resource.plural}", "#{resource.path}.:format", index_action_options)
if resource.deprecate_name_prefix?
map.deprecated_named_route("#{resource.name_prefix}#{resource.plural}", "#{resource.plural}")
map.deprecated_named_route("formatted_#{resource.name_prefix}#{resource.plural}", "formatted_#{resource.plural}")
end
create_action_options = action_options_for("create", resource) create_action_options = action_options_for("create", resource)
map.connect(resource.path, create_action_options) map.connect(resource.path, create_action_options)
map.connect("#{resource.path}.:format", create_action_options) map.connect("#{resource.path}.:format", create_action_options)
@ -351,11 +391,37 @@ module ActionController
actions.each do |action| actions.each do |action|
action_options = action_options_for(action, resource, method) action_options = action_options_for(action, resource, method)
if action == :new if action == :new
map.named_route("#{resource.name_prefix}new_#{resource.singular}", resource.new_path, action_options)
map.named_route("formatted_#{resource.name_prefix}new_#{resource.singular}", "#{resource.new_path}.:format", action_options) unless resource.old_name_prefix.blank?
map.deprecated_named_route("new_#{resource.name_prefix}#{resource.singular}", "#{resource.old_name_prefix}new_#{resource.singular}")
map.deprecated_named_route("formatted_new_#{resource.name_prefix}#{resource.singular}", "formatted_#{resource.old_name_prefix}new_#{resource.singular}")
end
if resource.deprecate_name_prefix?
map.deprecated_named_route("new_#{resource.name_prefix}#{resource.singular}", "new_#{resource.singular}")
map.deprecated_named_route("formatted_new_#{resource.name_prefix}#{resource.singular}", "formatted_new_#{resource.singular}")
end
map.named_route("new_#{resource.name_prefix}#{resource.singular}", resource.new_path, action_options)
map.named_route("formatted_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}.:format", action_options)
else else
map.named_route("#{resource.name_prefix}#{action}_new_#{resource.singular}", "#{resource.new_path};#{action}", action_options)
map.named_route("formatted_#{resource.name_prefix}#{action}_new_#{resource.singular}", "#{resource.new_path}.:format;#{action}", action_options) unless resource.old_name_prefix.blank?
map.deprecated_named_route("#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.old_name_prefix}#{action}_new_#{resource.singular}")
map.deprecated_named_route("formatted_#{action}_new_#{resource.name_prefix}#{resource.singular}", "formatted_#{resource.old_name_prefix}#{action}_new_#{resource.singular}")
end
if resource.deprecate_name_prefix?
map.deprecated_named_route("#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{action}_new_#{resource.singular}")
map.deprecated_named_route("formatted_#{action}_new_#{resource.name_prefix}#{resource.singular}", "formatted_#{action}_new_#{resource.singular}")
end
map.named_route("#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}", action_options)
map.connect("#{resource.new_path};#{action}", action_options)
map.connect("#{resource.new_path}.:format;#{action}", action_options)
map.named_route("formatted_#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}.:format", action_options)
end end
end end
end end
@ -365,8 +431,22 @@ module ActionController
resource.member_methods.each do |method, actions| resource.member_methods.each do |method, actions|
actions.each do |action| actions.each do |action|
action_options = action_options_for(action, resource, method) action_options = action_options_for(action, resource, method)
map.named_route("#{resource.name_prefix}#{action}_#{resource.singular}", "#{resource.member_path};#{action}", action_options)
map.named_route("formatted_#{resource.name_prefix}#{action}_#{resource.singular}", "#{resource.member_path}.:format;#{action}",action_options) unless resource.old_name_prefix.blank?
map.deprecated_named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.old_name_prefix}#{action}_#{resource.singular}")
map.deprecated_named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "formatted_#{resource.old_name_prefix}#{action}_#{resource.singular}")
end
if resource.deprecate_name_prefix?
map.deprecated_named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{action}_#{resource.singular}")
map.deprecated_named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "formatted_#{action}_#{resource.singular}")
end
map.named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action}", action_options)
map.connect("#{resource.member_path};#{action}", action_options)
map.connect("#{resource.member_path}.:format;#{action}", action_options)
map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action}.:format", action_options)
end end
end end
@ -374,6 +454,11 @@ module ActionController
map.named_route("#{resource.name_prefix}#{resource.singular}", resource.member_path, show_action_options) map.named_route("#{resource.name_prefix}#{resource.singular}", resource.member_path, show_action_options)
map.named_route("formatted_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}.:format", show_action_options) map.named_route("formatted_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}.:format", show_action_options)
if resource.deprecate_name_prefix?
map.deprecated_named_route("#{resource.name_prefix}#{resource.singular}", "#{resource.singular}")
map.deprecated_named_route("formatted_#{resource.name_prefix}#{resource.singular}", "formatted_#{resource.singular}")
end
update_action_options = action_options_for("update", resource) update_action_options = action_options_for("update", resource)
map.connect(resource.member_path, update_action_options) map.connect(resource.member_path, update_action_options)
map.connect("#{resource.member_path}.:format", update_action_options) map.connect("#{resource.member_path}.:format", update_action_options)

View file

@ -454,21 +454,12 @@ module ActionController
def build_query_string(hash, only_keys = nil) def build_query_string(hash, only_keys = nil)
elements = [] elements = []
only_keys ||= hash.keys (only_keys || hash.keys).each do |key|
if value = hash[key]
only_keys.each do |key| elements << value.to_query(key)
value = hash[key] or next
key = CGI.escape key.to_s
if value.class == Array
key << '[]'
else
value = [ value ]
end end
value.each { |val| elements << "#{key}=#{CGI.escape(val.to_param.to_s)}" }
end end
elements.empty? ? '' : "?#{elements.sort * '&'}"
query_string = "?#{elements.join("&")}" unless elements.empty?
query_string || ""
end end
# Write the real recognition implementation and then resend the message. # Write the real recognition implementation and then resend the message.
@ -668,7 +659,7 @@ module ActionController
end end
def extract_value def extract_value
"#{local_name} = hash[:#{key}] #{"|| #{default.inspect}" if default}" "#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}"
end end
def value_check def value_check
if default # Then we know it won't be nil if default # Then we know it won't be nil
@ -990,6 +981,10 @@ module ActionController
@set.add_named_route(name, path, options) @set.add_named_route(name, path, options)
end end
def deprecated_named_route(name, deprecated_name, options = {})
@set.add_deprecated_named_route(name, deprecated_name)
end
# Added deprecation notice for anyone who already added a named route called "root". # Added deprecation notice for anyone who already added a named route called "root".
# It'll be used as a shortcut for map.connect '' in Rails 2.0. # It'll be used as a shortcut for map.connect '' in Rails 2.0.
def root(*args, &proc) def root(*args, &proc)
@ -1056,6 +1051,38 @@ module ActionController
Array(destinations).each { |dest| dest.send :include, @module } Array(destinations).each { |dest| dest.send :include, @module }
end end
def define_deprecated_named_route_methods(name, deprecated_name)
[:url, :path].each do |kind|
@module.send :module_eval, <<-end_eval # We use module_eval to avoid leaks
def #{url_helper_name(deprecated_name, kind)}(*args)
ActiveSupport::Deprecation.warn(
'The named route "#{url_helper_name(deprecated_name, kind)}" uses a format that has been deprecated. ' +
'You should use "#{url_helper_name(name, kind)}" instead.', caller
)
send :#{url_helper_name(name, kind)}, *args
end
def #{hash_access_name(deprecated_name, kind)}(*args)
ActiveSupport::Deprecation.warn(
'The named route "#{hash_access_name(deprecated_name, kind)}" uses a format that has been deprecated. ' +
'You should use "#{hash_access_name(name, kind)}" instead.', caller
)
send :#{hash_access_name(name, kind)}, *args
end
end_eval
end
end
private private
def url_helper_name(name, kind = :url) def url_helper_name(name, kind = :url)
:"#{name}_#{kind}" :"#{name}_#{kind}"
@ -1178,6 +1205,10 @@ module ActionController
named_routes[name] = add_route(path, options) named_routes[name] = add_route(path, options)
end end
def add_deprecated_named_route(name, deprecated_name)
named_routes.define_deprecated_named_route_methods(name, deprecated_name)
end
def options_as_params(options) def options_as_params(options)
# If an explicit :controller was given, always make :action explicit # If an explicit :controller was given, always make :action explicit
# too, so that action expiry works as expected for things like # too, so that action expiry works as expected for things like
@ -1190,10 +1221,9 @@ module ActionController
# #
# great fun, eh? # great fun, eh?
options_as_params = options[:controller] ? { :action => "index" } : {} options_as_params = options.clone
options.each do |k, value| options_as_params[:action] ||= 'index' if options[:controller]
options_as_params[k] = value.to_param options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action]
end
options_as_params options_as_params
end end
@ -1224,6 +1254,9 @@ module ActionController
options = options_as_params(options) options = options_as_params(options)
expire_on = build_expiry(options, recall) expire_on = build_expiry(options, recall)
if options[:controller]
options[:controller] = options[:controller].to_s
end
# if the controller has changed, make sure it changes relative to the # if the controller has changed, make sure it changes relative to the
# current controller module, if any. In other words, if we're currently # current controller module, if any. In other words, if we're currently
# on admin/get, and the new controller is 'set', the new controller # on admin/get, and the new controller is 'set', the new controller

View file

@ -24,6 +24,7 @@ module ActionController #:nodoc:
attr_accessor :cookies, :session_options attr_accessor :cookies, :session_options
attr_accessor :query_parameters, :request_parameters, :path, :session, :env attr_accessor :query_parameters, :request_parameters, :path, :session, :env
attr_accessor :host attr_accessor :host
attr_reader :request_uri_overridden
def initialize(query_parameters = nil, request_parameters = nil, session = nil) def initialize(query_parameters = nil, request_parameters = nil, session = nil)
@query_parameters = query_parameters || {} @query_parameters = query_parameters || {}
@ -67,12 +68,14 @@ module ActionController #:nodoc:
# Used to check AbstractRequest's request_uri functionality. # Used to check AbstractRequest's request_uri functionality.
# Disables the use of @path and @request_uri so superclass can handle those. # Disables the use of @path and @request_uri so superclass can handle those.
def set_REQUEST_URI(value) def set_REQUEST_URI(value)
@request_uri_overridden = true
@env["REQUEST_URI"] = value @env["REQUEST_URI"] = value
@request_uri = nil @request_uri = nil
@path = nil @path = nil
end end
def request_uri=(uri) def request_uri=(uri)
@env["REQUEST_URI"] = uri
@request_uri = uri @request_uri = uri
@path = uri.split("?").first @path = uri.split("?").first
end end
@ -426,12 +429,12 @@ module ActionController #:nodoc:
end end
def build_request_uri(action, parameters) def build_request_uri(action, parameters)
unless @request.env['REQUEST_URI'] unless @request.request_uri_overridden
options = @controller.send(:rewrite_options, parameters) options = @controller.send(:rewrite_options, parameters)
options.update(:only_path => true, :action => action) options.update(:only_path => true, :action => action)
url = ActionController::UrlRewriter.new(@request, parameters) url = ActionController::UrlRewriter.new(@request, parameters)
@request.set_REQUEST_URI(url.rewrite(options)) @request.request_uri = url.rewrite(options)
end end
end end

View file

@ -52,8 +52,9 @@ module ActionController
# Delete the unused options to prevent their appearance in the query string # Delete the unused options to prevent their appearance in the query string
[:protocol, :host, :port].each { |k| options.delete k } [:protocol, :host, :port].each { |k| options.delete k }
end end
anchor = "##{options.delete(:anchor)}" if options.key?(:anchor)
url << Routing::Routes.generate(options, {}) url << Routing::Routes.generate(options, {})
return url return "#{url}#{anchor}"
end end
end end
@ -76,6 +77,7 @@ module ActionController
alias_method :to_s, :to_str alias_method :to_s, :to_str
private private
# Given a path and options, returns a rewritten URL string
def rewrite_url(path, options) def rewrite_url(path, options)
rewritten_url = "" rewritten_url = ""
unless options[:only_path] unless options[:only_path]
@ -91,6 +93,7 @@ module ActionController
rewritten_url rewritten_url
end end
# Given a Hash of options, generates a route
def rewrite_path(options) def rewrite_path(options)
options = options.symbolize_keys options = options.symbolize_keys
options.update(options[:params].symbolize_keys) if options[:params] options.update(options[:params].symbolize_keys) if options[:params]

View file

@ -95,6 +95,7 @@ module ActionController #:nodoc:
response.headers.update(options[:add_headers]) if options[:add_headers] response.headers.update(options[:add_headers]) if options[:add_headers]
unless performed? unless performed?
render(options[:render]) if options[:render] render(options[:render]) if options[:render]
options[:redirect_to] = self.send(options[:redirect_to]) if options[:redirect_to].is_a? Symbol
redirect_to(options[:redirect_to]) if options[:redirect_to] redirect_to(options[:redirect_to]) if options[:redirect_to]
end end
return false return false

View file

@ -2,7 +2,7 @@ module ActionPack #:nodoc:
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 1 MAJOR = 1
MINOR = 13 MINOR = 13
TINY = 3 TINY = 5
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View file

@ -148,7 +148,7 @@ module ActionView #:nodoc:
# #
# This refreshes the sidebar, removes a person element and highlights the user list. # This refreshes the sidebar, removes a person element and highlights the user list.
# #
# See the ActionView::Helpers::PrototypeHelper::JavaScriptGenerator documentation for more details. # See the ActionView::Helpers::PrototypeHelper::GeneratorMethods documentation for more details.
class Base class Base
include ERB::Util include ERB::Util
@ -160,7 +160,7 @@ module ActionView #:nodoc:
attr_internal *ActionController::Base::DEPRECATED_INSTANCE_VARIABLES attr_internal *ActionController::Base::DEPRECATED_INSTANCE_VARIABLES
# Specify trim mode for the ERB compiler. Defaults to '-'. # Specify trim mode for the ERB compiler. Defaults to '-'.
# See ERB documentation for suitable values. # See ERb documentation for suitable values.
@@erb_trim_mode = '-' @@erb_trim_mode = '-'
cattr_accessor :erb_trim_mode cattr_accessor :erb_trim_mode
@ -191,17 +191,17 @@ module ActionView #:nodoc:
end end
include CompiledTemplates include CompiledTemplates
# maps inline templates to their method names # Maps inline templates to their method names
@@method_names = {} @@method_names = {}
# map method names to their compile time # Map method names to their compile time
@@compile_time = {} @@compile_time = {}
# map method names to the names passed in local assigns so far # Map method names to the names passed in local assigns so far
@@template_args = {} @@template_args = {}
# count the number of inline templates # Count the number of inline templates
@@inline_template_count = 0 @@inline_template_count = 0
# maps template paths without extension to their file extension returned by pick_template_extension. # Maps template paths without extension to their file extension returned by pick_template_extension.
# if for a given path, path.ext1 and path.ext2 exist on the file system, the order of extensions # If for a given path, path.ext1 and path.ext2 exist on the file system, the order of extensions
# used by pick_template_extension determines whether ext1 or ext2 will be stored # used by pick_template_extension determines whether ext1 or ext2 will be stored.
@@cached_template_extension = {} @@cached_template_extension = {}
class ObjectWrapper < Struct.new(:value) #:nodoc: class ObjectWrapper < Struct.new(:value) #:nodoc:
@ -305,7 +305,6 @@ module ActionView #:nodoc:
# Render the provided template with the given local assigns. If the template has not been rendered with the provided # Render the provided template with the given local assigns. If the template has not been rendered with the provided
# local assigns yet, or if the template has been updated on disk, then the template will be compiled to a method. # local assigns yet, or if the template has been updated on disk, then the template will be compiled to a method.
# #
# Either, but not both, of template and file_path may be nil. If file_path is given, the template # Either, but not both, of template and file_path may be nil. If file_path is given, the template
# will only be read if it has to be compiled. # will only be read if it has to be compiled.
# #
@ -371,10 +370,12 @@ module ActionView #:nodoc:
end end
private private
# Builds a string holding the full path of the template including extension
def full_template_path(template_path, extension) def full_template_path(template_path, extension)
"#{@base_path}/#{template_path}.#{extension}" "#{@base_path}/#{template_path}.#{extension}"
end end
# Asserts the existence of a template.
def template_exists?(template_path, extension) def template_exists?(template_path, extension)
file_path = full_template_path(template_path, extension) file_path = full_template_path(template_path, extension)
@@method_names.has_key?(file_path) || FileTest.exists?(file_path) @@method_names.has_key?(file_path) || FileTest.exists?(file_path)
@ -389,6 +390,7 @@ module ActionView #:nodoc:
@@cache_template_extensions && @@cached_template_extension[template_path] @@cache_template_extensions && @@cached_template_extension[template_path]
end end
# Determines the template's file extension, such as rhtml, rxml, or rjs.
def find_template_extension_for(template_path) def find_template_extension_for(template_path)
if match = delegate_template_exists?(template_path) if match = delegate_template_exists?(template_path)
match.first.to_sym match.first.to_sym
@ -405,6 +407,7 @@ module ActionView #:nodoc:
File.read(template_path) File.read(template_path)
end end
# Evaluate the local assigns and pushes them to the view.
def evaluate_assigns def evaluate_assigns
unless @assigns_added unless @assigns_added
assign_variables_from_controller assign_variables_from_controller
@ -416,6 +419,7 @@ module ActionView #:nodoc:
handler.new(self).render(template, local_assigns) handler.new(self).render(template, local_assigns)
end end
# Assigns instance variables from the controller to the view.
def assign_variables_from_controller def assign_variables_from_controller
@assigns.each { |key, value| instance_variable_set("@#{key}", value) } @assigns.each { |key, value| instance_variable_set("@#{key}", value) }
end end
@ -427,10 +431,10 @@ module ActionView #:nodoc:
((args = @@template_args[render_symbol]) && local_assigns.all? { |k,_| args.has_key?(k) }) ((args = @@template_args[render_symbol]) && local_assigns.all? { |k,_| args.has_key?(k) })
end end
# Check whether compilation is necessary. # Method to check whether template compilation is necessary.
# Compile if the inline template or file has not been compiled yet. # The template will be compiled if the inline template or file has not been compiled yet,
# Or if local_assigns has a new key, which isn't supported by the compiled code yet. # if local_assigns has a new key, which isn't supported by the compiled code yet,
# Or if the file has changed on disk and checking file mods hasn't been disabled. # or if the file has changed on disk and checking file mods hasn't been disabled.
def compile_template?(template, file_name, local_assigns) def compile_template?(template, file_name, local_assigns)
method_key = file_name || template method_key = file_name || template
render_symbol = @@method_names[method_key] render_symbol = @@method_names[method_key]
@ -445,14 +449,15 @@ module ActionView #:nodoc:
end end
end end
# Create source code for given template # Method to create the source code for a given template.
def create_template_source(extension, template, render_symbol, locals) def create_template_source(extension, template, render_symbol, locals)
if template_requires_setup?(extension) if template_requires_setup?(extension)
body = case extension.to_sym body = case extension.to_sym
when :rxml when :rxml
"controller.response.content_type ||= 'application/xml'\n" + "controller.response.content_type ||= 'application/xml'\n" +
"xml = Builder::XmlMarkup.new(:indent => 2)\n" + "xml ||= Builder::XmlMarkup.new(:indent => 2)\n" +
template template +
"\nxml.target!\n"
when :rjs when :rjs
"controller.response.content_type ||= 'text/javascript'\n" + "controller.response.content_type ||= 'text/javascript'\n" +
"update_page do |page|\n#{template}\nend" "update_page do |page|\n#{template}\nend"
@ -473,11 +478,11 @@ module ActionView #:nodoc:
"def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend" "def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend"
end end
def template_requires_setup?(extension) def template_requires_setup?(extension) #:nodoc:
templates_requiring_setup.include? extension.to_s templates_requiring_setup.include? extension.to_s
end end
def templates_requiring_setup def templates_requiring_setup #:nodoc:
%w(rxml rjs) %w(rxml rjs)
end end
@ -501,6 +506,7 @@ module ActionView #:nodoc:
end end
end end
# Compile and evaluate the template's code
def compile_template(extension, template, file_name, local_assigns) def compile_template(extension, template, file_name, local_assigns)
render_symbol = assign_method_name(extension, template, file_name) render_symbol = assign_method_name(extension, template, file_name)
render_source = create_template_source(extension, template, render_symbol, local_assigns.keys) render_source = create_template_source(extension, template, render_symbol, local_assigns.keys)

View file

@ -3,14 +3,14 @@ module ActionView
# CompiledTemplates modules hold methods that have been compiled. # CompiledTemplates modules hold methods that have been compiled.
# Templates are compiled into these methods so that they do not need to be # Templates are compiled into these methods so that they do not need to be
# re-read and re-parsed each request. # read and parsed for each request.
# #
# Each template may be compiled into one or more methods. Each method accepts a given # Each template may be compiled into one or more methods. Each method accepts a given
# set of parameters which is used to implement local assigns passing. # set of parameters which is used to implement local assigns passing.
# #
# To use a compiled template module, create a new instance and include it into the class # To use a compiled template module, create a new instance and include it into the class
# in which you want the template to be rendered. # in which you want the template to be rendered.
class CompiledTemplates < Module #:nodoc: class CompiledTemplates < Module
attr_reader :method_names attr_reader :method_names
def initialize def initialize

View file

@ -13,17 +13,18 @@ module ActionView
# is a great of making the record quickly available for editing, but likely to prove lackluster for a complicated real-world form. # is a great of making the record quickly available for editing, but likely to prove lackluster 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 # 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 module ActiveRecordHelper
# Returns a default input tag for the type of object returned by the method. Example # Returns a default input tag for the type of object returned by the method. For example, let's say you have a model
# (title is a VARCHAR column and holds "Hello World"): # that has an attribute +title+ of type VARCHAR column, and this instance holds "Hello World":
# input("post", "title") => # input("post", "title") =>
# <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /> # <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
def input(record_name, method, options = {}) def input(record_name, method, options = {})
InstanceTag.new(record_name, method, self).to_tag(options) InstanceTag.new(record_name, method, self).to_tag(options)
end end
# Returns an entire form with input tags and everything for a specified Active Record object. Example # Returns an entire form with all needed input tags for a specified Active Record object. For example, let's say you
# (post is a new record that has a title using VARCHAR and a body using TEXT): # have a table model <tt>Post</tt> with attributes named <tt>title</tt> of type <tt>VARCHAR</tt> and <tt>body</tt> of type <tt>TEXT</tt>:
# form("post") => # form("post")
# That line would yield a form like the following:
# <form action='/post/create' method='post'> # <form action='/post/create' method='post'>
# <p> # <p>
# <label for="post_title">Title</label><br /> # <label for="post_title">Title</label><br />
@ -32,14 +33,13 @@ module ActionView
# <p> # <p>
# <label for="post_body">Body</label><br /> # <label for="post_body">Body</label><br />
# <textarea cols="40" id="post_body" name="post[body]" rows="20"> # <textarea cols="40" id="post_body" name="post[body]" rows="20">
# Back to the hill and over it again!
# </textarea> # </textarea>
# </p> # </p>
# <input type='submit' value='Create' /> # <input type='submit' value='Create' />
# </form> # </form>
# #
# It's possible to specialize the form builder by using a different action name and by supplying another # 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): # block renderer. For example, let's say you have a model <tt>Entry</tt> with an attribute <tt>message</tt> of type <tt>VARCHAR</tt>:
# #
# form("entry", :action => "sign", :input_block => # form("entry", :action => "sign", :input_block =>
# Proc.new { |record, column| "#{column.human_name}: #{input(record, column.name)}<br />" }) => # Proc.new { |record, column| "#{column.human_name}: #{input(record, column.name)}<br />" }) =>
@ -74,16 +74,16 @@ module ActionView
content_tag('form', contents, :action => action, :method => 'post', :enctype => options[:multipart] ? 'multipart/form-data': nil) content_tag('form', contents, :action => action, :method => 'post', :enctype => options[:multipart] ? 'multipart/form-data': nil)
end end
# Returns a string containing the error message attached to the +method+ on the +object+, if one exists. # 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+ # This error message is wrapped in a <tt>DIV</tt> tag, which can be extended to include a +prepend_text+ and/or +append_text+
# to properly introduce the error and a +css_class+ to style it accordingly. Examples (post has an error message # (to properly explain the error), and a +css_class+ to style it accordingly. As an example, let's say you have a model
# "can't be empty" on the title attribute): # +post+ that has an error message on the +title+ attribute:
# #
# <%= error_message_on "post", "title" %> => # <%= error_message_on "post", "title" %> =>
# <div class="formError">can't be empty</div> # <div class="formError">can't be empty</div>
# #
# <%= error_message_on "post", "title", "Title simply ", " (or it won't work)", "inputError" %> => # <%= 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> # <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") def error_message_on(object, method, prepend_text = "", append_text = "", css_class = "formError")
if (obj = instance_variable_get("@#{object}")) && (errors = obj.errors.on(method)) if (obj = instance_variable_get("@#{object}")) && (errors = obj.errors.on(method))
content_tag("div", "#{prepend_text}#{errors.is_a?(Array) ? errors.first : errors}#{append_text}", :class => css_class) content_tag("div", "#{prepend_text}#{errors.is_a?(Array) ? errors.first : errors}#{append_text}", :class => css_class)
@ -92,11 +92,11 @@ module ActionView
end end
end end
# Returns a string with a div containing all of the error messages for the objects located as instance variables by the names # Returns a string with a <tt>DIV</tt> containing all of the error messages for the objects located as instance variables by the names
# given. If more than one object is specified, the errors for the objects are displayed in the order that the object names are # given. If more than one object is specified, the errors for the objects are displayed in the order that the object names are
# provided. # provided.
# #
# This div can be tailored by the following options: # This <tt>DIV</tt> can be tailored by the following options:
# #
# * <tt>header_tag</tt> - Used for the header of the error div (default: h2) # * <tt>header_tag</tt> - Used for the header of the error div (default: h2)
# * <tt>id</tt> - The id of the error div (default: errorExplanation) # * <tt>id</tt> - The id of the error div (default: errorExplanation)
@ -105,12 +105,12 @@ module ActionView
# any text that you prefer. If <tt>object_name</tt> is not set, the name of # any text that you prefer. If <tt>object_name</tt> is not set, the name of
# the first object will be used. # the first object will be used.
# #
# Specifying one object: # To specify the display for one object, you simply provide its name as a parameter. For example, for the +User+ model:
# #
# error_messages_for 'user' # error_messages_for 'user'
# #
# Specifying more than one object (and using the name 'user' in the # To specify more than one object, you simply list them; optionally, you can add an extra +object_name+ parameter, which
# header as the <tt>object_name</tt> instead of 'user_common'): # be the name in the header.
# #
# error_messages_for 'user_common', 'user', :object_name => 'user' # error_messages_for 'user_common', 'user', :object_name => 'user'
# #

View file

@ -3,6 +3,16 @@ module ActionView
# Provides a set of methods for making it easier to locate problems. # Provides a set of methods for making it easier to locate problems.
module DebugHelper module DebugHelper
# Returns a <pre>-tag set with the +object+ dumped by YAML. Very readable way to inspect an object. # Returns a <pre>-tag set with the +object+ dumped by YAML. Very readable way to inspect an object.
# my_hash = {'first' => 1, 'second' => 'two', 'third' => [1,2,3]}
# debug(my_hash)
# => <pre class='debug_dump'>---
# first: 1
# second: two
# third:
# - 1
# - 2
# - 3
# </pre>
def debug(object) def debug(object)
begin begin
Marshal::dump(object) Marshal::dump(object)

View file

@ -2,6 +2,9 @@ module ActionView
module Helpers module Helpers
module PrototypeHelper module PrototypeHelper
# Method to execute an element update using Prototype.
# DEPRECATION WARNING: This helper has been depercated; use RJS instead.
# See ActionView::Helpers::PrototypeHelper::JavaScriptGenerator::GeneratorMethods for more information.
def update_element_function(element_id, options = {}, &block) def update_element_function(element_id, options = {}, &block)
content = escape_javascript(options[:content] || '') content = escape_javascript(options[:content] || '')
content = escape_javascript(capture(&block)) if block content = escape_javascript(capture(&block)) if block

View file

@ -250,8 +250,10 @@ module ActionView
return function return function
end end
# Observes the field with the DOM ID specified by +field_id+ and makes # Observes the field with the DOM ID specified by +field_id+ and calls a
# an Ajax call when its contents have changed. # callback when its contents have changed. The default callback is an
# Ajax call. By default the value of the observed field is sent as a
# parameter with the Ajax call.
# #
# Required +options+ are either of: # Required +options+ are either of:
# <tt>:url</tt>:: +url_for+-style options for the action to call # <tt>:url</tt>:: +url_for+-style options for the action to call
@ -268,14 +270,24 @@ module ActionView
# <tt>:update</tt>:: Specifies the DOM ID of the element whose # <tt>:update</tt>:: Specifies the DOM ID of the element whose
# innerHTML should be updated with the # innerHTML should be updated with the
# XMLHttpRequest response text. # XMLHttpRequest response text.
# <tt>:with</tt>:: A JavaScript expression specifying the # <tt>:with</tt>:: A JavaScript expression specifying the parameters
# parameters for the XMLHttpRequest. This defaults # for the XMLHttpRequest. The default is to send the
# to 'value', which in the evaluated context # key and value of the observed field. Any custom
# refers to the new field value. If you specify a # expressions should return a valid URL query string.
# string without a "=", it'll be extended to mean # The value of the field is stored in the JavaScript
# the form key that the value should be assigned to. # variable +value+.
# So :with => "term" gives "'term'=value". If a "=" is #
# present, no extension will happen. # Examples
#
# :with => "'my_custom_key=' + value"
# :with => "'person[name]=' + prompt('New name')"
# :with => "Form.Element.serialize('other-field')"
#
# Finally
# :with => 'name'
# is shorthand for
# :with => "'name=' + value"
# This essentially just changes the key of the parameter.
# <tt>:on</tt>:: Specifies which event handler to observe. By default, # <tt>:on</tt>:: Specifies which event handler to observe. By default,
# it's set to "changed" for text fields and areas and # it's set to "changed" for text fields and areas and
# "click" for radio buttons and checkboxes. With this, # "click" for radio buttons and checkboxes. With this,
@ -292,10 +304,14 @@ module ActionView
end end
end end
# Like +observe_field+, but operates on an entire form identified by the # Observes the form with the DOM ID specified by +form_id+ and calls a
# DOM ID +form_id+. +options+ are the same as +observe_field+, except # callback when its contents have changed. The default callback is an
# the default value of the <tt>:with</tt> option evaluates to the # Ajax call. By default all fields of the observed field are sent as
# serialized (request string) value of the form. # parameters with the Ajax call.
#
# The +options+ for +observe_form+ are the same as the options for
# +observe_field+. The JavaScript variable +value+ available to the
# <tt>:with</tt> option is set to the serialized form by default.
def observe_form(form_id, options = {}) def observe_form(form_id, options = {})
if options[:frequency] if options[:frequency]
build_observer('Form.Observer', form_id, options) build_observer('Form.Observer', form_id, options)
@ -660,10 +676,10 @@ module ActionView
end end
def build_observer(klass, name, options = {}) def build_observer(klass, name, options = {})
if options[:with] && !options[:with].include?("=") if options[:with] && (options[:with] !~ /[=(.]/)
options[:with] = "'#{options[:with]}=' + value" options[:with] = "'#{options[:with]}=' + value"
else else
options[:with] ||= 'value' if options[:update] options[:with] ||= 'value' unless options[:function]
end end
callback = options[:function] || remote_function(options) callback = options[:function] || remote_function(options)

View file

@ -6,6 +6,8 @@ class PaginationTest < ActiveRecordTestCase
class PaginationController < ActionController::Base class PaginationController < ActionController::Base
self.template_root = "#{File.dirname(__FILE__)}/../fixtures/" self.template_root = "#{File.dirname(__FILE__)}/../fixtures/"
around_filter :silence_deprecation_warnings
def simple_paginate def simple_paginate
@topic_pages, @topics = paginate(:topics) @topic_pages, @topics = paginate(:topics)
render :nothing => true render :nothing => true
@ -68,6 +70,13 @@ class PaginationTest < ActiveRecordTestCase
render :nothing => true render :nothing => true
end end
def silence_deprecation_warnings
ActiveSupport::Deprecation.silence do
yield
end
end
def rescue_errors(e) raise e end def rescue_errors(e) raise e end
def rescue_action(e) raise end def rescue_action(e) raise end

View file

@ -19,6 +19,8 @@ class ActionPackAssertionsController < ActionController::Base
def redirect_to_controller() redirect_to :controller => "elsewhere", :action => "flash_me"; end def redirect_to_controller() redirect_to :controller => "elsewhere", :action => "flash_me"; end
def redirect_to_controller_with_symbol() redirect_to :controller => :elsewhere, :action => :flash_me; end
def redirect_to_path() redirect_to '/some/path' end def redirect_to_path() redirect_to '/some/path' end
def redirect_to_named_route() redirect_to route_one_url end def redirect_to_named_route() redirect_to route_one_url end
@ -555,6 +557,17 @@ class ActionPackAssertionsControllerTest < Test::Unit::TestCase
assert_redirected_to 'http://test.host/some/path' assert_redirected_to 'http://test.host/some/path'
end end
def test_assert_redirection_with_symbol
process :redirect_to_controller_with_symbol
assert_nothing_raised {
assert_redirected_to :controller => "elsewhere", :action => "flash_me"
}
process :redirect_to_controller_with_symbol
assert_nothing_raised {
assert_redirected_to :controller => :elsewhere, :action => :flash_me
}
end
def test_redirected_to_with_nested_controller def test_redirected_to_with_nested_controller
@controller = Admin::InnerModuleController.new @controller = Admin::InnerModuleController.new
get :redirect_to_absolute_controller get :redirect_to_absolute_controller

View file

@ -39,7 +39,10 @@ class AddressesTest < Test::Unit::TestCase
end end
def test_list def test_list
# because pagination is deprecated
ActiveSupport::Deprecation.silence do
get :list get :list
end
assert_equal "We only need to get this far!", @response.body.chomp assert_equal "We only need to get this far!", @response.body.chomp
end end
end end

View file

@ -88,7 +88,7 @@ class ControllerInstanceTests < Test::Unit::TestCase
# Mocha adds methods to Object which are then included in the public_instance_methods # Mocha adds methods to Object which are then included in the public_instance_methods
# This method hides those from the controller so the above tests won't know the difference # This method hides those from the controller so the above tests won't know the difference
def hide_mocha_methods_from_controller(controller) def hide_mocha_methods_from_controller(controller)
mocha_methods = [:expects, :metaclass, :mocha, :mocha_inspect, :reset_mocha, :stubba_object, :stubba_method, :stubs, :verify] mocha_methods = [:expects, :metaclass, :mocha, :mocha_inspect, :reset_mocha, :stubba_object, :stubba_method, :stubs, :verify, :__is_a__, :__metaclass__]
controller.class.send(:hide_action, *mocha_methods) controller.class.send(:hide_action, *mocha_methods)
end end

View file

@ -97,6 +97,7 @@ class ActionCachingTestController < ActionController::Base
caches_action :index caches_action :index
def index def index
sleep 0.01
@cache_this = Time.now.to_f.to_s @cache_this = Time.now.to_f.to_s
render :text => @cache_this render :text => @cache_this
end end
@ -195,7 +196,7 @@ class ActionCacheTest < Test::Unit::TestCase
def test_xml_version_of_resource_is_treated_as_different_cache def test_xml_version_of_resource_is_treated_as_different_cache
@mock_controller.mock_url_for = 'http://example.org/posts/' @mock_controller.mock_url_for = 'http://example.org/posts/'
@mock_controller.mock_path = '/posts/index.xml' @mock_controller.mock_path = '/posts/index.xml'
path_object = @path_class.new(@mock_controller) path_object = @path_class.new(@mock_controller, {})
assert_equal 'xml', path_object.extension assert_equal 'xml', path_object.extension
assert_equal 'example.org/posts/index.xml', path_object.path assert_equal 'example.org/posts/index.xml', path_object.path
end end
@ -204,7 +205,7 @@ class ActionCacheTest < Test::Unit::TestCase
@mock_controller.mock_url_for = 'http://example.org/' @mock_controller.mock_url_for = 'http://example.org/'
@mock_controller.mock_path = '/' @mock_controller.mock_path = '/'
assert_equal 'example.org/index', @path_class.path_for(@mock_controller) assert_equal 'example.org/index', @path_class.path_for(@mock_controller, {})
end end
def test_file_extensions def test_file_extensions

View file

@ -31,6 +31,11 @@ class CookieTest < Test::Unit::TestCase
cookies.delete("user_name") cookies.delete("user_name")
end end
def delete_cookie_with_path
cookies.delete("user_name", :path => '/beaten')
render_text "hello world"
end
def rescue_action(e) def rescue_action(e)
raise unless ActionController::MissingTemplate # No templates here, and we don't care about the output raise unless ActionController::MissingTemplate # No templates here, and we don't care about the output
end end
@ -85,4 +90,10 @@ class CookieTest < Test::Unit::TestCase
assert_equal "david", jar["user_name"] assert_equal "david", jar["user_name"]
assert_equal nil, jar["something_else"] assert_equal nil, jar["something_else"]
end end
def test_delete_cookie_with_path
get :delete_cookie_with_path
assert_equal "/beaten", @response.headers["cookie"].first.path
assert_not_equal "/", @response.headers["cookie"].first.path
end
end end

View file

@ -1,6 +1,12 @@
require File.dirname(__FILE__) + '/../../abstract_unit' require File.dirname(__FILE__) + '/../../abstract_unit'
class DeprecatedBaseMethodsTest < Test::Unit::TestCase class DeprecatedBaseMethodsTest < Test::Unit::TestCase
# ActiveRecord model mock to test pagination deprecation
class DummyModel
def self.find(*args) [] end
def self.count(*args) 0 end
end
class Target < ActionController::Base class Target < ActionController::Base
def deprecated_symbol_parameter_to_url_for def deprecated_symbol_parameter_to_url_for
redirect_to(url_for(:home_url, "superstars")) redirect_to(url_for(:home_url, "superstars"))
@ -18,6 +24,11 @@ class DeprecatedBaseMethodsTest < Test::Unit::TestCase
this_method_doesnt_exist this_method_doesnt_exist
end end
def pagination
paginate :dummy_models, :class_name => 'DeprecatedBaseMethodsTest::DummyModel'
render :nothing => true
end
def rescue_action(e) raise e end def rescue_action(e) raise e end
end end
@ -27,6 +38,7 @@ class DeprecatedBaseMethodsTest < Test::Unit::TestCase
@request = ActionController::TestRequest.new @request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new @response = ActionController::TestResponse.new
@controller = Target.new @controller = Target.new
@controller.logger = Logger.new(nil) unless @controller.logger
end end
def test_deprecated_symbol_parameter_to_url_for def test_deprecated_symbol_parameter_to_url_for
@ -57,4 +69,10 @@ class DeprecatedBaseMethodsTest < Test::Unit::TestCase
error = Test::Unit::Error.new('testing ur doodz', e) error = Test::Unit::Error.new('testing ur doodz', e)
assert_not_deprecated { error.message } assert_not_deprecated { error.message }
end end
def test_pagination_deprecation
assert_deprecated('svn://errtheblog.com/svn/plugins/classic_pagination') do
get :pagination
end
end
end end

View file

@ -16,6 +16,7 @@ class FilterParamTest < Test::Unit::TestCase
assert @controller.respond_to?(:filter_parameters) assert @controller.respond_to?(:filter_parameters)
test_hashes = [[{},{},[]], test_hashes = [[{},{},[]],
[{'foo'=>nil},{'foo'=>nil},[]],
[{'foo'=>'bar'},{'foo'=>'bar'},[]], [{'foo'=>'bar'},{'foo'=>'bar'},[]],
[{'foo'=>'bar'},{'foo'=>'bar'},%w'food'], [{'foo'=>'bar'},{'foo'=>'bar'},%w'food'],
[{'foo'=>'bar'},{'foo'=>'[FILTERED]'},%w'foo'], [{'foo'=>'bar'},{'foo'=>'[FILTERED]'},%w'foo'],

View file

@ -131,6 +131,14 @@ class FilterTest < Test::Unit::TestCase
before_filter(ConditionalClassFilter, :ensure_login, Proc.new {|c| c.assigns["ran_proc_filter1"] = true }, :except => :show_without_filter) { |c| c.assigns["ran_proc_filter2"] = true} before_filter(ConditionalClassFilter, :ensure_login, Proc.new {|c| c.assigns["ran_proc_filter1"] = true }, :except => :show_without_filter) { |c| c.assigns["ran_proc_filter2"] = true}
end end
class EmptyFilterChainController < TestController
self.filter_chain.clear
def show
@action_executed = true
render :text => "yawp!"
end
end
class PrependingController < TestController class PrependingController < TestController
prepend_before_filter :wonderful_life prepend_before_filter :wonderful_life
# skip_before_filter :fire_flash # skip_before_filter :fire_flash
@ -234,6 +242,14 @@ class FilterTest < Test::Unit::TestCase
around_filter AroundFilter.new around_filter AroundFilter.new
end end
class BeforeAfterClassFilterController < PrependingController
begin
filter = AroundFilter.new
before_filter filter
after_filter filter
end
end
class MixedFilterController < PrependingController class MixedFilterController < PrependingController
cattr_accessor :execution_log cattr_accessor :execution_log
@ -285,6 +301,101 @@ class FilterTest < Test::Unit::TestCase
end end
end end
class PrependingBeforeAndAfterController < ActionController::Base
prepend_before_filter :before_all
prepend_after_filter :after_all
before_filter :between_before_all_and_after_all
def before_all
@ran_filter ||= []
@ran_filter << 'before_all'
end
def after_all
@ran_filter ||= []
@ran_filter << 'after_all'
end
def between_before_all_and_after_all
@ran_filter ||= []
@ran_filter << 'between_before_all_and_after_all'
end
def show
render :text => 'hello'
end
end
class NonYieldingAroundFilterController < ActionController::Base
before_filter :filter_one
around_filter :non_yielding_filter
before_filter :filter_two
after_filter :filter_three
def index
render :inline => "index"
end
#make sure the controller complains
def rescue_action(e); raise e; end
private
def filter_one
@filters ||= []
@filters << "filter_one"
end
def filter_two
@filters << "filter_two"
end
def non_yielding_filter
@filters << "zomg it didn't yield"
@filter_return_value
end
def filter_three
@filters << "filter_three"
end
end
def test_non_yielding_around_filters_not_returning_false_do_not_raise
controller = NonYieldingAroundFilterController.new
controller.instance_variable_set "@filter_return_value", true
assert_nothing_raised do
test_process(controller, "index")
end
end
def test_non_yielding_around_filters_returning_false_do_not_raise
controller = NonYieldingAroundFilterController.new
controller.instance_variable_set "@filter_return_value", false
assert_nothing_raised do
test_process(controller, "index")
end
end
def test_after_filters_are_not_run_if_around_filter_returns_false
controller = NonYieldingAroundFilterController.new
controller.instance_variable_set "@filter_return_value", false
test_process(controller, "index")
assert_equal ["filter_one", "zomg it didn't yield"], controller.assigns['filters']
end
def test_after_filters_are_not_run_if_around_filter_does_not_yield
controller = NonYieldingAroundFilterController.new
controller.instance_variable_set "@filter_return_value", true
test_process(controller, "index")
assert_equal ["filter_one", "zomg it didn't yield"], controller.assigns['filters']
end
def test_empty_filter_chain
assert_equal 0, EmptyFilterChainController.filter_chain.size
assert test_process(EmptyFilterChainController).template.assigns['action_executed']
end
def test_added_filter_to_inheritance_graph def test_added_filter_to_inheritance_graph
assert_equal [ :ensure_login ], TestController.before_filters assert_equal [ :ensure_login ], TestController.before_filters
end end
@ -373,6 +484,12 @@ class FilterTest < Test::Unit::TestCase
assert controller.template.assigns["after_ran"] assert controller.template.assigns["after_ran"]
end end
def test_before_after_class_filter
controller = test_process(BeforeAfterClassFilterController)
assert controller.template.assigns["before_ran"]
assert controller.template.assigns["after_ran"]
end
def test_having_properties_in_around_filter def test_having_properties_in_around_filter
controller = test_process(AroundFilterController) controller = test_process(AroundFilterController)
assert_equal "before and after", controller.template.assigns["execution_log"] assert_equal "before and after", controller.template.assigns["execution_log"]
@ -412,6 +529,12 @@ class FilterTest < Test::Unit::TestCase
end end
end end
def test_running_prepended_before_and_after_filter
assert_equal 3, PrependingBeforeAndAfterController.filter_chain.length
response = test_process(PrependingBeforeAndAfterController)
assert_equal %w( before_all between_before_all_and_after_all after_all ), response.template.assigns["ran_filter"]
end
def test_conditional_skipping_of_filters def test_conditional_skipping_of_filters
assert_nil test_process(ConditionalSkippingController, "login").template.assigns["ran_filter"] assert_nil test_process(ConditionalSkippingController, "login").template.assigns["ran_filter"]
assert_equal %w( ensure_login find_user ), test_process(ConditionalSkippingController, "change_password").template.assigns["ran_filter"] assert_equal %w( ensure_login find_user ), test_process(ConditionalSkippingController, "change_password").template.assigns["ran_filter"]

View file

@ -11,7 +11,7 @@ require 'stubba'
module ActionController module ActionController
module Integration module Integration
class Session class Session
def process def process(*args)
end end
def generic_url_rewriter def generic_url_rewriter
@ -56,8 +56,7 @@ class SessionTest < Test::Unit::TestCase
@session.expects(:get).with(path,args) @session.expects(:get).with(path,args)
redirects = [true, true, false] @session.stubs(:redirect?).returns(true).then.returns(true).then.returns(false)
@session.stubs(:redirect?).returns(lambda { redirects.shift })
@session.expects(:follow_redirect!).times(2) @session.expects(:follow_redirect!).times(2)
@session.stubs(:status).returns(200) @session.stubs(:status).returns(200)
@ -69,8 +68,7 @@ class SessionTest < Test::Unit::TestCase
@session.expects(:post).with(path,args) @session.expects(:post).with(path,args)
redirects = [true, true, false] @session.stubs(:redirect?).returns(true).then.returns(true).then.returns(false)
@session.stubs(:redirect?).returns(lambda { redirects.shift })
@session.expects(:follow_redirect!).times(2) @session.expects(:follow_redirect!).times(2)
@session.stubs(:status).returns(200) @session.stubs(:status).returns(200)
@ -134,15 +132,102 @@ class SessionTest < Test::Unit::TestCase
@session.head(path,params,headers) @session.head(path,params,headers)
end end
def test_xml_http_request def test_xml_http_request_deprecated_call
path = "/index"; params = "blah"; headers = {:location => 'blah'} path = "/index"; params = "blah"; headers = {:location => 'blah'}
headers_after_xhr = headers.merge( headers_after_xhr = headers.merge(
"X-Requested-With" => "XMLHttpRequest", "X-Requested-With" => "XMLHttpRequest",
"Accept" => "text/javascript, text/html, application/xml, text/xml, */*" "Accept" => "text/javascript, text/html, application/xml, text/xml, */*"
) )
@session.expects(:post).with(path,params,headers_after_xhr) @session.expects(:process).with(:post,path,params,headers_after_xhr)
@session.xml_http_request(path,params,headers) assert_deprecated { @session.xml_http_request(path,params,headers) }
end end
def test_xml_http_request_get
path = "/index"; params = "blah"; headers = {:location => 'blah'}
headers_after_xhr = headers.merge(
"X-Requested-With" => "XMLHttpRequest",
"Accept" => "text/javascript, text/html, application/xml, text/xml, */*"
)
@session.expects(:process).with(:get,path,params,headers_after_xhr)
@session.xml_http_request(:get,path,params,headers)
end
def test_xml_http_request_post
path = "/index"; params = "blah"; headers = {:location => 'blah'}
headers_after_xhr = headers.merge(
"X-Requested-With" => "XMLHttpRequest",
"Accept" => "text/javascript, text/html, application/xml, text/xml, */*"
)
@session.expects(:process).with(:post,path,params,headers_after_xhr)
@session.xml_http_request(:post,path,params,headers)
end
def test_xml_http_request_put
path = "/index"; params = "blah"; headers = {:location => 'blah'}
headers_after_xhr = headers.merge(
"X-Requested-With" => "XMLHttpRequest",
"Accept" => "text/javascript, text/html, application/xml, text/xml, */*"
)
@session.expects(:process).with(:put,path,params,headers_after_xhr)
@session.xml_http_request(:put,path,params,headers)
end
def test_xml_http_request_delete
path = "/index"; params = "blah"; headers = {:location => 'blah'}
headers_after_xhr = headers.merge(
"X-Requested-With" => "XMLHttpRequest",
"Accept" => "text/javascript, text/html, application/xml, text/xml, */*"
)
@session.expects(:process).with(:delete,path,params,headers_after_xhr)
@session.xml_http_request(:delete,path,params,headers)
end
def test_xml_http_request_head
path = "/index"; params = "blah"; headers = {:location => 'blah'}
headers_after_xhr = headers.merge(
"X-Requested-With" => "XMLHttpRequest",
"Accept" => "text/javascript, text/html, application/xml, text/xml, */*"
)
@session.expects(:process).with(:head,path,params,headers_after_xhr)
@session.xml_http_request(:head,path,params,headers)
end
end
class IntegrationTestTest < Test::Unit::TestCase
def setup
@test = ::ActionController::IntegrationTest.new(:default_test)
@test.class.stubs(:fixture_table_names).returns([])
@session = @test.open_session
end
def test_opens_new_session
@test.class.expects(:fixture_table_names).times(2).returns(['foo'])
session1 = @test.open_session { |sess| }
session2 = @test.open_session # implicit session
assert_equal ::ActionController::Integration::Session, session1.class
assert_equal ::ActionController::Integration::Session, session2.class
assert_not_equal session1, session2
end
end
# Tests that integration tests don't call Controller test methods for processing.
# Integration tests have their own setup and teardown.
class IntegrationTestUsesCorrectClass < ActionController::IntegrationTest
def self.fixture_table_names
[]
end
def test_integration_methods_called
%w( get post head put delete ).each do |verb|
assert_nothing_raised("'#{verb}' should use integration test methods") { send(verb, '/') }
end
end
end end
# TODO # TODO

View file

@ -69,10 +69,6 @@ class TestController < ActionController::Base
render "test/hello" render "test/hello"
end end
def heading
head :ok
end
def greeting def greeting
# let's just rely on the template # let's just rely on the template
end end
@ -290,50 +286,8 @@ class RenderTest < Test::Unit::TestCase
assert_equal "Goodbye, Local David", @response.body assert_equal "Goodbye, Local David", @response.body
end end
def test_render_200_should_set_etag
get :render_hello_world_from_variable
assert_equal etag_for("hello david"), @response.headers['Etag']
end
def test_render_against_etag_request_should_304_when_match
@request.headers["HTTP_IF_NONE_MATCH"] = etag_for("hello david")
get :render_hello_world_from_variable
assert_equal "304 Not Modified", @response.headers['Status']
assert @response.body.empty?
end
def test_render_against_etag_request_should_200_when_no_match
@request.headers["HTTP_IF_NONE_MATCH"] = etag_for("hello somewhere else")
get :render_hello_world_from_variable
assert_equal "200 OK", @response.headers['Status']
assert !@response.body.empty?
end
def test_render_with_etag
get :render_hello_world_from_variable
expected_etag = "\"#{MD5.new("hello david").to_s}\""
assert_equal expected_etag, @response.headers['Etag']
@request.headers["HTTP_IF_NONE_MATCH"] = expected_etag
get :render_hello_world_from_variable
assert_equal "304 Not Modified", @response.headers['Status']
@request.headers["HTTP_IF_NONE_MATCH"] = "\"diftag\""
get :render_hello_world_from_variable
assert_equal "200 OK", @response.headers['Status']
end
def render_with_404_shouldnt_have_etag
get :render_custom_code
assert_nil @response.headers['Etag']
end
protected protected
def assert_deprecated_render(&block) def assert_deprecated_render(&block)
assert_deprecated(/render/, &block) assert_deprecated(/render/, &block)
end end
def etag_for(text)
"\"#{MD5.new(text).to_s}\""
end
end end

View file

@ -12,6 +12,7 @@ class CommentsController < ResourcesController; end
class AccountController < ResourcesController; end class AccountController < ResourcesController; end
class AdminController < ResourcesController; end class AdminController < ResourcesController; end
class ProductsController < ResourcesController; end
class ResourcesTest < Test::Unit::TestCase class ResourcesTest < Test::Unit::TestCase
def test_should_arrange_actions def test_should_arrange_actions
@ -63,7 +64,7 @@ class ResourcesTest < Test::Unit::TestCase
end end
end end
def test_multile_with_path_prefix def test_multiple_with_path_prefix
with_restful_routing :messages, :comments, :path_prefix => '/thread/:thread_id' do with_restful_routing :messages, :comments, :path_prefix => '/thread/:thread_id' do
assert_simply_restful_for :messages, :path_prefix => 'thread/5/', :options => { :thread_id => '5' } assert_simply_restful_for :messages, :path_prefix => 'thread/5/', :options => { :thread_id => '5' }
assert_simply_restful_for :comments, :path_prefix => 'thread/5/', :options => { :thread_id => '5' } assert_simply_restful_for :comments, :path_prefix => 'thread/5/', :options => { :thread_id => '5' }
@ -78,7 +79,7 @@ class ResourcesTest < Test::Unit::TestCase
def test_with_collection_action def test_with_collection_action
rss_options = {:action => 'rss'} rss_options = {:action => 'rss'}
rss_path = "/messages;rss" rss_path = "/messages/rss"
actions = { 'a' => :put, 'b' => :post, 'c' => :delete } actions = { 'a' => :put, 'b' => :post, 'c' => :delete }
with_restful_routing :messages, :collection => { :rss => :get }.merge(actions) do with_restful_routing :messages, :collection => { :rss => :get }.merge(actions) do
@ -86,14 +87,14 @@ class ResourcesTest < Test::Unit::TestCase
assert_routing rss_path, options.merge(rss_options) assert_routing rss_path, options.merge(rss_options)
actions.each do |action, method| actions.each do |action, method|
assert_recognizes(options.merge(:action => action), :path => "/messages;#{action}", :method => method) assert_recognizes(options.merge(:action => action), :path => "/messages/#{action}", :method => method)
end end
end end
assert_restful_named_routes_for :messages do |options| assert_restful_named_routes_for :messages do |options|
assert_named_route rss_path, :rss_messages_path, rss_options assert_named_route rss_path, :rss_messages_path, rss_options
actions.keys.each do |action| actions.keys.each do |action|
assert_named_route "/messages;#{action}", "#{action}_messages_path", :action => action assert_named_route "/messages/#{action}", "#{action}_messages_path", :action => action
end end
end end
end end
@ -103,7 +104,7 @@ class ResourcesTest < Test::Unit::TestCase
[:put, :post].each do |method| [:put, :post].each do |method|
with_restful_routing :messages, :member => { :mark => method } do with_restful_routing :messages, :member => { :mark => method } do
mark_options = {:action => 'mark', :id => '1'} mark_options = {:action => 'mark', :id => '1'}
mark_path = "/messages/1;mark" mark_path = "/messages/1/mark"
assert_restful_routes_for :messages do |options| assert_restful_routes_for :messages do |options|
assert_recognizes(options.merge(mark_options), :path => mark_path, :method => method) assert_recognizes(options.merge(mark_options), :path => mark_path, :method => method)
end end
@ -120,7 +121,7 @@ class ResourcesTest < Test::Unit::TestCase
with_restful_routing :messages, :member => { :mark => method, :unmark => method } do with_restful_routing :messages, :member => { :mark => method, :unmark => method } do
%w(mark unmark).each do |action| %w(mark unmark).each do |action|
action_options = {:action => action, :id => '1'} action_options = {:action => action, :id => '1'}
action_path = "/messages/1;#{action}" action_path = "/messages/1/#{action}"
assert_restful_routes_for :messages do |options| assert_restful_routes_for :messages do |options|
assert_recognizes(options.merge(action_options), :path => action_path, :method => method) assert_recognizes(options.merge(action_options), :path => action_path, :method => method)
end end
@ -136,7 +137,7 @@ class ResourcesTest < Test::Unit::TestCase
def test_with_new_action def test_with_new_action
with_restful_routing :messages, :new => { :preview => :post } do with_restful_routing :messages, :new => { :preview => :post } do
preview_options = {:action => 'preview'} preview_options = {:action => 'preview'}
preview_path = "/messages/new;preview" preview_path = "/messages/new/preview"
assert_restful_routes_for :messages do |options| assert_restful_routes_for :messages do |options|
assert_recognizes(options.merge(preview_options), :path => preview_path, :method => :post) assert_recognizes(options.merge(preview_options), :path => preview_path, :method => :post)
end end
@ -178,9 +179,11 @@ class ResourcesTest < Test::Unit::TestCase
assert_simply_restful_for :threads assert_simply_restful_for :threads
assert_simply_restful_for :messages, assert_simply_restful_for :messages,
:path_prefix => 'threads/1/', :path_prefix => 'threads/1/',
:name_prefix => 'thread_',
:options => { :thread_id => '1' } :options => { :thread_id => '1' }
assert_simply_restful_for :comments, assert_simply_restful_for :comments,
:path_prefix => 'threads/1/messages/2/', :path_prefix => 'threads/1/messages/2/',
:name_prefix => 'thread_message_',
:options => { :thread_id => '1', :message_id => '2' } :options => { :thread_id => '1', :message_id => '2' }
end end
end end
@ -219,7 +222,7 @@ class ResourcesTest < Test::Unit::TestCase
end end
assert_singleton_restful_for :admin assert_singleton_restful_for :admin
assert_singleton_restful_for :account, :path_prefix => 'admin/' assert_singleton_restful_for :account, :path_prefix => 'admin/', :name_prefix => 'admin_'
end end
end end
@ -227,7 +230,7 @@ class ResourcesTest < Test::Unit::TestCase
[:put, :post].each do |method| [:put, :post].each do |method|
with_singleton_resources :account, :member => { :reset => method } do with_singleton_resources :account, :member => { :reset => method } do
reset_options = {:action => 'reset'} reset_options = {:action => 'reset'}
reset_path = "/account;reset" reset_path = "/account/reset"
assert_singleton_routes_for :account do |options| assert_singleton_routes_for :account do |options|
assert_recognizes(options.merge(reset_options), :path => reset_path, :method => method) assert_recognizes(options.merge(reset_options), :path => reset_path, :method => method)
end end
@ -244,7 +247,7 @@ class ResourcesTest < Test::Unit::TestCase
with_singleton_resources :account, :member => { :reset => method, :disable => method } do with_singleton_resources :account, :member => { :reset => method, :disable => method } do
%w(reset disable).each do |action| %w(reset disable).each do |action|
action_options = {:action => action} action_options = {:action => action}
action_path = "/account;#{action}" action_path = "/account/#{action}"
assert_singleton_routes_for :account do |options| assert_singleton_routes_for :account do |options|
assert_recognizes(options.merge(action_options), :path => action_path, :method => method) assert_recognizes(options.merge(action_options), :path => action_path, :method => method)
end end
@ -266,7 +269,7 @@ class ResourcesTest < Test::Unit::TestCase
end end
assert_singleton_restful_for :account assert_singleton_restful_for :account
assert_simply_restful_for :messages, :path_prefix => 'account/' assert_simply_restful_for :messages, :path_prefix => 'account/', :name_prefix => 'account_'
end end
end end
@ -279,7 +282,7 @@ class ResourcesTest < Test::Unit::TestCase
end end
assert_singleton_restful_for :account, :path_prefix => '7/', :options => { :site_id => '7' } assert_singleton_restful_for :account, :path_prefix => '7/', :options => { :site_id => '7' }
assert_simply_restful_for :messages, :path_prefix => '7/account/', :options => { :site_id => '7' } assert_simply_restful_for :messages, :path_prefix => '7/account/', :name_prefix => 'account_', :options => { :site_id => '7' }
end end
end end
@ -292,7 +295,7 @@ class ResourcesTest < Test::Unit::TestCase
end end
assert_simply_restful_for :threads assert_simply_restful_for :threads
assert_singleton_restful_for :admin, :path_prefix => 'threads/5/', :options => { :thread_id => '5' } assert_singleton_restful_for :admin, :path_prefix => 'threads/5/', :name_prefix => 'thread_', :options => { :thread_id => '5' }
end end
end end
@ -312,6 +315,181 @@ class ResourcesTest < Test::Unit::TestCase
end end
end end
def test_resource_action_separator
with_routing do |set|
set.draw do |map|
map.resources :messages, :collection => {:search => :get}, :new => {:preview => :any}, :name_prefix => 'thread_', :path_prefix => '/threads/:thread_id'
map.resource :account, :member => {:login => :get}, :new => {:preview => :any}, :name_prefix => 'admin_', :path_prefix => '/admin'
end
action_separator = ActionController::Base.resource_action_separator
assert_simply_restful_for :messages, :name_prefix => 'thread_', :path_prefix => 'threads/1/', :options => { :thread_id => '1' }
assert_named_route "/threads/1/messages#{action_separator}search", "search_thread_messages_path", {}
assert_named_route "/threads/1/messages/new", "new_thread_message_path", {}
assert_named_route "/threads/1/messages/new#{action_separator}preview", "preview_new_thread_message_path", {}
assert_singleton_restful_for :account, :name_prefix => 'admin_', :path_prefix => 'admin/'
assert_named_route "/admin/account#{action_separator}login", "login_admin_account_path", {}
assert_named_route "/admin/account/new", "new_admin_account_path", {}
assert_named_route "/admin/account/new#{action_separator}preview", "preview_new_admin_account_path", {}
end
end
def test_new_style_named_routes_for_resource
with_routing do |set|
set.draw do |map|
map.resources :messages, :collection => {:search => :get}, :new => {:preview => :any}, :name_prefix => 'thread_', :path_prefix => '/threads/:thread_id'
end
assert_simply_restful_for :messages, :name_prefix => 'thread_', :path_prefix => 'threads/1/', :options => { :thread_id => '1' }
assert_named_route "/threads/1/messages/search", "search_thread_messages_path", {}
assert_named_route "/threads/1/messages/new", "new_thread_message_path", {}
assert_named_route "/threads/1/messages/new/preview", "preview_new_thread_message_path", {}
end
end
def test_new_style_named_routes_for_singleton_resource
with_routing do |set|
set.draw do |map|
map.resource :account, :member => {:login => :get}, :new => {:preview => :any}, :name_prefix => 'admin_', :path_prefix => '/admin'
end
assert_singleton_restful_for :account, :name_prefix => 'admin_', :path_prefix => 'admin/'
assert_named_route "/admin/account/login", "login_admin_account_path", {}
assert_named_route "/admin/account/new", "new_admin_account_path", {}
assert_named_route "/admin/account/new/preview", "preview_new_admin_account_path", {}
end
end
def test_should_add_deprecated_named_routes_for_resource
with_routing do |set|
set.draw do |map|
map.resources :messages, :collection => {:search => :get}, :new => {:preview => :any}, :name_prefix => 'thread_', :path_prefix => '/threads/:thread_id'
end
assert_simply_restful_for :messages, :name_prefix => 'thread_', :path_prefix => 'threads/1/', :options => { :thread_id => '1' }
assert_deprecated do
assert_named_route "/threads/1/messages/search", "thread_search_messages_path", {}
assert_named_route "/threads/1/messages/new", "thread_new_message_path", {}
assert_named_route "/threads/1/messages/new/preview", "thread_preview_new_message_path", {}
end
end
end
def test_should_add_deprecated_named_routes_for_singleton_resource
with_routing do |set|
set.draw do |map|
map.resource :account, :member => {:login => :get}, :new => {:preview => :any}, :name_prefix => 'admin_', :path_prefix => '/admin'
end
assert_singleton_restful_for :account, :name_prefix => 'admin_', :path_prefix => 'admin/'
assert_deprecated do
assert_named_route "/admin/account/login", "admin_login_account_path", {}
assert_named_route "/admin/account/new", "admin_new_account_path", {}
assert_named_route "/admin/account/new/preview", "admin_preview_new_account_path", {}
end
end
end
def test_should_add_deprecated_named_routes_for_nested_resources
with_routing do |set|
set.draw do |map|
map.resources :threads do |map|
map.resources :messages do |map|
map.resources :comments
end
end
end
assert_simply_restful_for :threads
assert_simply_restful_for :messages,
:path_prefix => 'threads/1/',
:name_prefix => 'thread_',
:options => { :thread_id => '1' }
assert_simply_restful_for :comments,
:path_prefix => 'threads/1/messages/2/',
:name_prefix => 'thread_message_',
:options => { :thread_id => '1', :message_id => '2' }
assert_deprecated do
assert_named_route "/threads/1/messages", "messages_path", {}
assert_named_route "/threads/1/messages/1", "message_path", {:thread_id => '1', :id => '1'}
assert_named_route "/threads/1/messages/new", "new_message_path", {:thread_id => '1'}
assert_named_route "/threads/1/messages/1/edit", "edit_message_path", {:thread_id => '1', :id => '1'}
end
end
end
def test_should_add_deprecated_named_routes_for_nested_singleton_resources
with_routing do |set|
set.draw do |map|
map.resource :admin do |admin|
admin.resource :account
end
end
assert_singleton_restful_for :admin
assert_singleton_restful_for :account, :path_prefix => 'admin/', :name_prefix => 'admin_'
assert_deprecated do
assert_named_route "/admin/account", "account_path", {}
assert_named_route "/admin/account/new", "new_account_path", {}
assert_named_route "/admin/account/edit", "edit_account_path", {}
end
end
end
def test_should_add_deprecated_named_routes_for_nested_resources_in_singleton_resource
with_routing do |set|
set.draw do |map|
map.resource :account do |account|
account.resources :messages
end
end
assert_singleton_restful_for :account
assert_simply_restful_for :messages, :path_prefix => 'account/', :name_prefix => 'account_'
assert_deprecated do
assert_named_route "/account/messages", "messages_path", {}
assert_named_route "/account/messages/1", "message_path", {:id => '1'}
assert_named_route "/account/messages/new", "new_message_path", {}
assert_named_route "/account/messages/1/edit", "edit_message_path", {:id => '1'}
end
end
end
def test_should_add_deprecated_named_routes_for_nested_singleton_resource_in_resources
with_routing do |set|
set.draw do |map|
map.resources :threads do |thread|
thread.resource :admin
end
end
assert_simply_restful_for :threads
assert_singleton_restful_for :admin, :path_prefix => 'threads/5/', :name_prefix => 'thread_', :options => { :thread_id => '5' }
assert_deprecated do
assert_named_route "/threads/5/admin", "admin_path", {}
assert_named_route "/threads/5/admin/new", "new_admin_path", {}
assert_named_route "/threads/5/admin/edit", "edit_admin_path", {}
end
end
end
def test_should_add_deprecated_formatted_routes
with_routing do |set|
set.draw do |map|
map.resources :products, :collection => { :specials => :get }, :member => { :thumbnail => :get }
map.resource :account, :member => { :icon => :get }
end
assert_restful_routes_for :products do |options|
assert_recognizes options.merge({ :action => 'specials', :format => 'xml' }), :path => '/products.xml;specials', :method => :get
assert_recognizes options.merge({ :action => 'thumbnail', :format => 'jpg', :id => '1' }), :path => '/products/1.jpg;thumbnail', :method => :get
end
assert_singleton_restful_for :account do |options|
assert_recognizes options.merge({ :action => 'icon', :format => 'jpg' }), :path => '/account.jpg;icon', :method => :get
end
end
end
protected protected
def with_restful_routing(*args) def with_restful_routing(*args)
with_routing do |set| with_routing do |set|
@ -344,8 +522,8 @@ class ResourcesTest < Test::Unit::TestCase
collection_path = "/#{options[:path_prefix]}#{controller_name}" collection_path = "/#{options[:path_prefix]}#{controller_name}"
member_path = "#{collection_path}/1" member_path = "#{collection_path}/1"
new_path = "#{collection_path}/new" new_path = "#{collection_path}/new"
edit_member_path = "#{member_path};edit" edit_member_path = "#{member_path}/edit"
formatted_edit_member_path = "#{member_path}.xml;edit" formatted_edit_member_path = "#{member_path}/edit.xml"
with_options(options[:options]) do |controller| with_options(options[:options]) do |controller|
controller.assert_routing collection_path, :action => 'index' controller.assert_routing collection_path, :action => 'index'
@ -395,13 +573,13 @@ class ResourcesTest < Test::Unit::TestCase
name_prefix = options[:name_prefix] name_prefix = options[:name_prefix]
assert_named_route "#{full_prefix}", "#{name_prefix}#{controller_name}_path", options[:options] assert_named_route "#{full_prefix}", "#{name_prefix}#{controller_name}_path", options[:options]
assert_named_route "#{full_prefix}/new", "#{name_prefix}new_#{singular_name}_path", options[:options] assert_named_route "#{full_prefix}/new", "new_#{name_prefix}#{singular_name}_path", options[:options]
assert_named_route "#{full_prefix}/1", "#{name_prefix}#{singular_name}_path", options[:options].merge(:id => '1') assert_named_route "#{full_prefix}/1", "#{name_prefix}#{singular_name}_path", options[:options].merge(:id => '1')
assert_named_route "#{full_prefix}/1;edit", "#{name_prefix}edit_#{singular_name}_path", options[:options].merge(:id => '1') assert_named_route "#{full_prefix}/1/edit", "edit_#{name_prefix}#{singular_name}_path", options[:options].merge(:id => '1')
assert_named_route "#{full_prefix}.xml", "formatted_#{name_prefix}#{controller_name}_path", options[:options].merge( :format => 'xml') assert_named_route "#{full_prefix}.xml", "formatted_#{name_prefix}#{controller_name}_path", options[:options].merge( :format => 'xml')
assert_named_route "#{full_prefix}/new.xml", "formatted_#{name_prefix}new_#{singular_name}_path", options[:options].merge( :format => 'xml') assert_named_route "#{full_prefix}/new.xml", "formatted_new_#{name_prefix}#{singular_name}_path", options[:options].merge( :format => 'xml')
assert_named_route "#{full_prefix}/1.xml", "formatted_#{name_prefix}#{singular_name}_path", options[:options].merge(:id => '1', :format => 'xml') assert_named_route "#{full_prefix}/1.xml", "formatted_#{name_prefix}#{singular_name}_path", options[:options].merge(:id => '1', :format => 'xml')
assert_named_route "#{full_prefix}/1.xml;edit", "formatted_#{name_prefix}edit_#{singular_name}_path", options[:options].merge(:id => '1', :format => 'xml') assert_named_route "#{full_prefix}/1/edit.xml", "formatted_edit_#{name_prefix}#{singular_name}_path", options[:options].merge(:id => '1', :format => 'xml')
yield options[:options] if block_given? yield options[:options] if block_given?
end end
@ -410,8 +588,8 @@ class ResourcesTest < Test::Unit::TestCase
full_path = "/#{options[:path_prefix]}#{singleton_name}" full_path = "/#{options[:path_prefix]}#{singleton_name}"
new_path = "#{full_path}/new" new_path = "#{full_path}/new"
edit_path = "#{full_path};edit" edit_path = "#{full_path}/edit"
formatted_edit_path = "#{full_path}.xml;edit" formatted_edit_path = "#{full_path}/edit.xml"
with_options options[:options] do |controller| with_options options[:options] do |controller|
controller.assert_routing full_path, :action => 'show' controller.assert_routing full_path, :action => 'show'
@ -448,13 +626,14 @@ class ResourcesTest < Test::Unit::TestCase
options[:options].delete :action options[:options].delete :action
full_path = "/#{options[:path_prefix]}#{singleton_name}" full_path = "/#{options[:path_prefix]}#{singleton_name}"
full_name = "#{options[:name_prefix]}#{singleton_name}"
assert_named_route "#{full_path}", "#{singleton_name}_path", options[:options] assert_named_route "#{full_path}", "#{full_name}_path", options[:options]
assert_named_route "#{full_path}/new", "new_#{singleton_name}_path", options[:options] assert_named_route "#{full_path}/new", "new_#{full_name}_path", options[:options]
assert_named_route "#{full_path};edit", "edit_#{singleton_name}_path", options[:options] assert_named_route "#{full_path}/edit", "edit_#{full_name}_path", options[:options]
assert_named_route "#{full_path}.xml", "formatted_#{singleton_name}_path", options[:options].merge(:format => 'xml') assert_named_route "#{full_path}.xml", "formatted_#{full_name}_path", options[:options].merge(:format => 'xml')
assert_named_route "#{full_path}/new.xml", "formatted_new_#{singleton_name}_path", options[:options].merge(:format => 'xml') assert_named_route "#{full_path}/new.xml", "formatted_new_#{full_name}_path", options[:options].merge(:format => 'xml')
assert_named_route "#{full_path}.xml;edit", "formatted_edit_#{singleton_name}_path", options[:options].merge(:format => 'xml') assert_named_route "#{full_path}/edit.xml", "formatted_edit_#{full_name}_path", options[:options].merge(:format => 'xml')
end end
def assert_named_route(expected, route, options) def assert_named_route(expected, route, options)

View file

@ -265,7 +265,7 @@ class LegacyRouteSetTests < Test::Unit::TestCase
map.content '/content/:query', :controller => 'content', :action => 'show' map.content '/content/:query', :controller => 'content', :action => 'show'
end end
exception = assert_raise(ActionController::RoutingError) { rs.generate(:controller => 'content', :action => 'show', :use_route => "content") } exception = assert_raise(ActionController::RoutingError) { rs.generate(:controller => 'content', :action => 'show', :use_route => "content") }
expected_message = %[content_url failed to generate from {:action=>"show", :controller=>"content"} - you may have ambiguous routes, or you may need to supply additional parameters for this route. content_url has the following required parameters: ["content", :query] - are they all satisifed?] expected_message = "content_url failed to generate from #{{:action=>"show", :controller=>"content"}.inspect} - you may have ambiguous routes, or you may need to supply additional parameters for this route. content_url has the following required parameters: [\"content\", :query] - are they all satisifed?"
assert_equal expected_message, exception.message assert_equal expected_message, exception.message
end end
@ -946,7 +946,7 @@ class RouteTest < Test::Unit::TestCase
end end
def test_expand_array_build_query_string def test_expand_array_build_query_string
assert_equal '?x[]=1&x[]=2', order_query_string(@route.build_query_string(:x => [1, 2])) assert_equal '?x%5B%5D=1&x%5B%5D=2', order_query_string(@route.build_query_string(:x => [1, 2]))
end end
def test_escape_spaces_build_query_string_selected_keys def test_escape_spaces_build_query_string_selected_keys

View file

@ -482,6 +482,22 @@ HTML
end end
end end
def test_request_uri_updates
get :test_params
uri = @request.request_uri
assert_equal @request.env['REQUEST_URI'], uri
get :test_uri
assert_not_equal uri, @request.request_uri
uri = @request.request_uri
assert_equal @request.env['REQUEST_URI'], uri
get :test_uri, :testing => true
assert_not_equal uri, @request.request_uri
uri = @request.request_uri
assert_equal @request.env['REQUEST_URI'], uri
end
protected protected
def with_foo_routing def with_foo_routing
with_routing do |set| with_routing do |set|

View file

@ -17,14 +17,11 @@ class UrlRewriterTests < Test::Unit::TestCase
assert_match %r(/hi/hi/2$), u assert_match %r(/hi/hi/2$), u
end end
def test_anchor
private assert_equal(
def split_query_string(str) 'http://test.host/c/a/i#anchor',
[str[0].chr] + str[1..-1].split(/&/).sort @rewriter.rewrite(:controller => 'c', :action => 'a', :id => 'i', :anchor => 'anchor')
end )
def assert_query_equal(q1, q2)
assert_equal(split_query_string(q1), split_query_string(q2))
end end
end end
@ -76,6 +73,12 @@ class UrlWriterTests < Test::Unit::TestCase
) )
end end
def test_anchor
assert_equal('/c/a#anchor',
W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => 'anchor')
)
end
def test_named_route def test_named_route
ActionController::Routing::Routes.draw do |map| ActionController::Routing::Routes.draw do |map|
map.home '/home/sweet/home/:user', :controller => 'home', :action => 'index' map.home '/home/sweet/home/:user', :controller => 'home', :action => 'index'
@ -112,4 +115,57 @@ class UrlWriterTests < Test::Unit::TestCase
ActionController::Routing::Routes.load! ActionController::Routing::Routes.load!
end end
def test_one_parameter
assert_equal('/c/a?param=val',
W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :param => 'val')
)
end
def test_two_parameters
url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :p1 => 'X1', :p2 => 'Y2')
params = extract_params(url)
assert_equal params[0], { :p1 => 'X1' }.to_query
assert_equal params[1], { :p2 => 'Y2' }.to_query
end
def test_hash_parameter
url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :query => {:name => 'Bob', :category => 'prof'})
params = extract_params(url)
assert_equal params[0], { 'query[category]' => 'prof' }.to_query
assert_equal params[1], { 'query[name]' => 'Bob' }.to_query
end
def test_array_parameter
url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :query => ['Bob', 'prof'])
params = extract_params(url)
assert_equal params[0], { 'query[]' => 'Bob' }.to_query
assert_equal params[1], { 'query[]' => 'prof' }.to_query
end
def test_hash_recursive_parameters
url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :query => {:person => {:name => 'Bob', :position => 'prof'}, :hobby => 'piercing'})
params = extract_params(url)
assert_equal params[0], { 'query[hobby]' => 'piercing' }.to_query
assert_equal params[1], { 'query[person][name]' => 'Bob' }.to_query
assert_equal params[2], { 'query[person][position]' => 'prof' }.to_query
end
def test_hash_recursive_and_array_parameters
url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :id => 101, :query => {:person => {:name => 'Bob', :position => ['prof', 'art director']}, :hobby => 'piercing'})
assert_match %r(^/c/a/101), url
params = extract_params(url)
assert_equal params[0], { 'query[hobby]' => 'piercing' }.to_query
assert_equal params[1], { 'query[person][name]' => 'Bob' }.to_query
assert_equal params[2], { 'query[person][position][]' => 'art director' }.to_query
assert_equal params[3], { 'query[person][position][]' => 'prof' }.to_query
end
def test_path_generation_for_symbol_parameter_keys
assert_generates("/image", :controller=> :image)
end
private
def extract_params(url)
url.split('?', 2).last.split('&')
end
end end

View file

@ -34,10 +34,17 @@ class VerificationTest < Test::Unit::TestCase
verify :only => :must_be_post, :method => :post, :render => { :status => 405, :text => "Must be post" }, :add_headers => { "Allow" => "POST" } verify :only => :must_be_post, :method => :post, :render => { :status => 405, :text => "Must be post" }, :add_headers => { "Allow" => "POST" }
verify :only => :guarded_one_for_named_route_test, :params => "one",
:redirect_to => :foo_url
def guarded_one def guarded_one
render :text => "#{params[:one]}" render :text => "#{params[:one]}"
end end
def guarded_one_for_named_route_test
render :text => "#{params[:one]}"
end
def guarded_with_flash def guarded_with_flash
render :text => "#{params[:one]}" render :text => "#{params[:one]}"
end end
@ -94,6 +101,14 @@ class VerificationTest < Test::Unit::TestCase
@controller = TestController.new @controller = TestController.new
@request = ActionController::TestRequest.new @request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new @response = ActionController::TestResponse.new
ActionController::Routing::Routes.add_named_route :foo, '/foo', :controller => 'test', :action => 'foo'
end
def test_no_deprecation_warning_for_named_route
assert_not_deprecated do
get :guarded_one_for_named_route_test, :two => "not one"
assert_redirected_to '/foo'
end
end end
def test_guarded_one_with_prereqs def test_guarded_one_with_prereqs

View file

@ -1,3 +1,4 @@
xml.html do xml.html do
xml.p "Hello" xml.p "Hello"
end end
"String return value"

View file

@ -165,7 +165,12 @@ class AssetTagHelperTest < Test::Unit::TestCase
def test_preset_empty_asset_id def test_preset_empty_asset_id
Object.send(:const_set, :RAILS_ROOT, File.dirname(__FILE__) + "/../fixtures/") Object.send(:const_set, :RAILS_ROOT, File.dirname(__FILE__) + "/../fixtures/")
# on windows, setting ENV["XXX"] to "" makes ENV["XXX"] return nil
if RUBY_PLATFORM =~ /win32/
ENV["RAILS_ASSET_ID"] = " " ENV["RAILS_ASSET_ID"] = " "
else
ENV["RAILS_ASSET_ID"] = ""
end
assert_equal %(<img alt="Rails" src="/images/rails.png" />), image_tag("rails.png") assert_equal %(<img alt="Rails" src="/images/rails.png" />), image_tag("rails.png")
end end

View file

@ -71,7 +71,12 @@ class CompiledTemplateTests < Test::Unit::TestCase
end end
def test_compile_time def test_compile_time
`echo '#{@a}' > #{@a}; echo '#{@b}' > #{@b}; ln -s #{@a} #{@s}` File.open(@a, "w"){|f| f.puts @a}
File.open(@b, "w"){|f| f.puts @b}
# windows doesn't support symlinks (even under cygwin)
windows = (RUBY_PLATFORM =~ /win32/)
`ln -s #{@a} #{@s}` unless windows
v = ActionView::Base.new v = ActionView::Base.new
v.base_path = '.' v.base_path = '.'
@ -79,47 +84,54 @@ class CompiledTemplateTests < Test::Unit::TestCase
sleep 1 sleep 1
t = Time.now t = Time.now
sleep 1
v.compile_and_render_template(:rhtml, '', @a) v.compile_and_render_template(:rhtml, '', @a)
v.compile_and_render_template(:rhtml, '', @b) v.compile_and_render_template(:rhtml, '', @b)
v.compile_and_render_template(:rhtml, '', @s) v.compile_and_render_template(:rhtml, '', @s) unless windows
a_n = v.method_names[@a] a_n = v.method_names[@a]
b_n = v.method_names[@b] b_n = v.method_names[@b]
s_n = v.method_names[@s] s_n = v.method_names[@s] unless windows
ct_a = v.compile_time[a_n]
ct_b = v.compile_time[b_n]
ct_s = v.compile_time[s_n] unless windows
# all of the files have changed since last compile # all of the files have changed since last compile
assert v.compile_time[a_n] > t assert v.compile_time[a_n] > t
assert v.compile_time[b_n] > t assert v.compile_time[b_n] > t
assert v.compile_time[s_n] > t assert v.compile_time[s_n] > t unless windows
sleep 1 sleep 1
t = Time.now
v.compile_and_render_template(:rhtml, '', @a) v.compile_and_render_template(:rhtml, '', @a)
v.compile_and_render_template(:rhtml, '', @b) v.compile_and_render_template(:rhtml, '', @b)
v.compile_and_render_template(:rhtml, '', @s) v.compile_and_render_template(:rhtml, '', @s) unless windows
# none of the files have changed since last compile # none of the files have changed since last compile
assert v.compile_time[a_n] < t # so they should not have been recmpiled
assert v.compile_time[b_n] < t assert_equal ct_a, v.compile_time[a_n]
assert v.compile_time[s_n] < t assert_equal ct_b, v.compile_time[b_n]
assert_equal ct_s, v.compile_time[s_n] unless windows
`rm #{@s}; ln -s #{@b} #{@s}` `rm #{@s}; ln -s #{@b} #{@s}` unless windows
v.compile_and_render_template(:rhtml, '', @a) v.compile_and_render_template(:rhtml, '', @a)
v.compile_and_render_template(:rhtml, '', @b) v.compile_and_render_template(:rhtml, '', @b)
v.compile_and_render_template(:rhtml, '', @s) v.compile_and_render_template(:rhtml, '', @s) unless windows
# the symlink has changed since last compile # the symlink has changed since last compile
assert v.compile_time[a_n] < t assert_equal ct_a, v.compile_time[a_n]
assert v.compile_time[b_n] < t assert_equal ct_b, v.compile_time[b_n]
assert v.compile_time[s_n] > t assert v.compile_time[s_n] > t unless windows
sleep 1 sleep 1
`touch #{@b}` FileUtils.touch @b
t = Time.now t = Time.now
sleep 1
v.compile_and_render_template(:rhtml, '', @a) v.compile_and_render_template(:rhtml, '', @a)
v.compile_and_render_template(:rhtml, '', @b) v.compile_and_render_template(:rhtml, '', @b)
v.compile_and_render_template(:rhtml, '', @s) v.compile_and_render_template(:rhtml, '', @s) unless windows
# the file at the end of the symlink has changed since last compile # the file at the end of the symlink has changed since last compile
# both the symlink and the file at the end of it should be recompiled # both the symlink and the file at the end of it should be recompiled
assert v.compile_time[a_n] < t assert v.compile_time[a_n] < t
assert v.compile_time[b_n] > t assert v.compile_time[b_n] > t
assert v.compile_time[s_n] > t assert v.compile_time[s_n] > t unless windows
end end
end end

View file

@ -36,14 +36,14 @@ class JavaScriptHelperTest < Test::Unit::TestCase
html = link_to_function( "Greet me!" ) do |page| html = link_to_function( "Greet me!" ) do |page|
page.replace_html 'header', "<h1>Greetings</h1>" page.replace_html 'header', "<h1>Greetings</h1>"
end end
assert_dom_equal %(<a href="#" onclick="Element.update(&quot;header&quot;, &quot;&lt;h1&gt;Greetings&lt;/h1&gt;&quot;);; return false;">Greet me!</a>), html assert_dom_equal %q(<a href="#" onclick="Element.update(&quot;header&quot;, &quot;\074h1\076Greetings\074/h1\076&quot;);; return false;">Greet me!</a>), html
end end
def test_link_to_function_with_rjs_block_and_options def test_link_to_function_with_rjs_block_and_options
html = link_to_function( "Greet me!", :class => "updater" ) do |page| html = link_to_function( "Greet me!", :class => "updater" ) do |page|
page.replace_html 'header', "<h1>Greetings</h1>" page.replace_html 'header', "<h1>Greetings</h1>"
end end
assert_dom_equal %(<a href="#" class="updater" onclick="Element.update(&quot;header&quot;, &quot;&lt;h1&gt;Greetings&lt;/h1&gt;&quot;);; return false;">Greet me!</a>), html assert_dom_equal %q(<a href="#" class="updater" onclick="Element.update(&quot;header&quot;, &quot;\074h1\076Greetings\074/h1\076&quot;);; return false;">Greet me!</a>), html
end end
def test_button_to_function def test_button_to_function
@ -55,13 +55,13 @@ class JavaScriptHelperTest < Test::Unit::TestCase
html = button_to_function( "Greet me!" ) do |page| html = button_to_function( "Greet me!" ) do |page|
page.replace_html 'header', "<h1>Greetings</h1>" page.replace_html 'header', "<h1>Greetings</h1>"
end end
assert_dom_equal %(<input type="button" onclick="Element.update(&quot;header&quot;, &quot;&lt;h1&gt;Greetings&lt;/h1&gt;&quot;);;" value="Greet me!" />), html assert_dom_equal %q(<input type="button" onclick="Element.update(&quot;header&quot;, &quot;\074h1\076Greetings\074/h1\076&quot;);;" value="Greet me!" />), html
end end
def test_button_to_function_with_rjs_block_and_options def test_button_to_function_with_rjs_block_and_options
html = button_to_function( "Greet me!", :class => "greeter" ) do |page| html = button_to_function( "Greet me!", :class => "greeter" ) do |page|
page.replace_html 'header', "<h1>Greetings</h1>" page.replace_html 'header', "<h1>Greetings</h1>"
end end
assert_dom_equal %(<input type="button" class="greeter" onclick="Element.update(&quot;header&quot;, &quot;&lt;h1&gt;Greetings&lt;/h1&gt;&quot;);;" value="Greet me!" />), html assert_dom_equal %q(<input type="button" class="greeter" onclick="Element.update(&quot;header&quot;, &quot;\074h1\076Greetings\074/h1\076&quot;);;" value="Greet me!" />), html
end end
end end

View file

@ -22,7 +22,7 @@ class NumberHelperTest < Test::Unit::TestCase
def test_number_to_currency def test_number_to_currency
assert_equal("$1,234,567,890.50", number_to_currency(1234567890.50)) assert_equal("$1,234,567,890.50", number_to_currency(1234567890.50))
assert_equal("$1,234,567,890.51", number_to_currency(1234567890.506)) assert_equal("$1,234,567,890.51", number_to_currency(1234567890.506))
assert_equal("$1,234,567,890", number_to_currency(1234567890.50, {:precision => 0})) assert_equal("$1,234,567,891", number_to_currency(1234567890.51, {:precision => 0}))
assert_equal("$1,234,567,890.5", number_to_currency(1234567890.50, {:precision => 1})) assert_equal("$1,234,567,890.5", number_to_currency(1234567890.50, {:precision => 1}))
assert_equal("&pound;1234567890,50", number_to_currency(1234567890.50, {:unit => "&pound;", :separator => ",", :delimiter => ""})) assert_equal("&pound;1234567890,50", number_to_currency(1234567890.50, {:unit => "&pound;", :separator => ",", :delimiter => ""}))
assert_equal("$1,234,567,890.50", number_to_currency("1234567890.50")) assert_equal("$1,234,567,890.50", number_to_currency("1234567890.50"))

View file

@ -125,7 +125,7 @@ class PrototypeHelperTest < Test::Unit::TestCase
end end
def test_observe_field def test_observe_field
assert_dom_equal %(<script type=\"text/javascript\">\n//<![CDATA[\nnew Form.Element.Observer('glass', 300, function(element, value) {new Ajax.Request('http://www.example.com/reorder_if_empty', {asynchronous:true, evalScripts:true})})\n//]]>\n</script>), assert_dom_equal %(<script type=\"text/javascript\">\n//<![CDATA[\nnew Form.Element.Observer('glass', 300, function(element, value) {new Ajax.Request('http://www.example.com/reorder_if_empty', {asynchronous:true, evalScripts:true, parameters:value})})\n//]]>\n</script>),
observe_field("glass", :frequency => 5.minutes, :url => { :action => "reorder_if_empty" }) observe_field("glass", :frequency => 5.minutes, :url => { :action => "reorder_if_empty" })
end end
@ -135,7 +135,7 @@ class PrototypeHelperTest < Test::Unit::TestCase
end end
def test_observe_form def test_observe_form
assert_dom_equal %(<script type=\"text/javascript\">\n//<![CDATA[\nnew Form.Observer('cart', 2, function(element, value) {new Ajax.Request('http://www.example.com/cart_changed', {asynchronous:true, evalScripts:true})})\n//]]>\n</script>), assert_dom_equal %(<script type=\"text/javascript\">\n//<![CDATA[\nnew Form.Observer('cart', 2, function(element, value) {new Ajax.Request('http://www.example.com/cart_changed', {asynchronous:true, evalScripts:true, parameters:value})})\n//]]>\n</script>),
observe_form("cart", :frequency => 2, :url => { :action => "cart_changed" }) observe_form("cart", :frequency => 2, :url => { :action => "cart_changed" })
end end
@ -170,23 +170,23 @@ class JavaScriptGeneratorTest < Test::Unit::TestCase
end end
def test_insert_html_with_string def test_insert_html_with_string
assert_equal 'new Insertion.Top("element", "<p>This is a test</p>");', assert_equal 'new Insertion.Top("element", "\074p\076This is a test\074/p\076");',
@generator.insert_html(:top, 'element', '<p>This is a test</p>') @generator.insert_html(:top, 'element', '<p>This is a test</p>')
assert_equal 'new Insertion.Bottom("element", "<p>This is a test</p>");', assert_equal 'new Insertion.Bottom("element", "\074p\076This is a test\074/p\076");',
@generator.insert_html(:bottom, 'element', '<p>This is a test</p>') @generator.insert_html(:bottom, 'element', '<p>This is a test</p>')
assert_equal 'new Insertion.Before("element", "<p>This is a test</p>");', assert_equal 'new Insertion.Before("element", "\074p\076This is a test\074/p\076");',
@generator.insert_html(:before, 'element', '<p>This is a test</p>') @generator.insert_html(:before, 'element', '<p>This is a test</p>')
assert_equal 'new Insertion.After("element", "<p>This is a test</p>");', assert_equal 'new Insertion.After("element", "\074p\076This is a test\074/p\076");',
@generator.insert_html(:after, 'element', '<p>This is a test</p>') @generator.insert_html(:after, 'element', '<p>This is a test</p>')
end end
def test_replace_html_with_string def test_replace_html_with_string
assert_equal 'Element.update("element", "<p>This is a test</p>");', assert_equal 'Element.update("element", "\074p\076This is a test\074/p\076");',
@generator.replace_html('element', '<p>This is a test</p>') @generator.replace_html('element', '<p>This is a test</p>')
end end
def test_replace_element_with_string def test_replace_element_with_string
assert_equal 'Element.replace("element", "<div id=\"element\"><p>This is a test</p></div>");', assert_equal 'Element.replace("element", "\074div id=\"element\"\076\074p\076This is a test\074/p\076\074/div\076");',
@generator.replace('element', '<div id="element"><p>This is a test</p></div>') @generator.replace('element', '<div id="element"><p>This is a test</p></div>')
end end
@ -241,12 +241,12 @@ class JavaScriptGeneratorTest < Test::Unit::TestCase
@generator.remove('foo', 'bar') @generator.remove('foo', 'bar')
@generator.replace_html('baz', '<p>This is a test</p>') @generator.replace_html('baz', '<p>This is a test</p>')
assert_equal <<-EOS.chomp, @generator.to_s expected = %q(new Insertion.Top("element", "\074p\076This is a test\074/p\076");
new Insertion.Top("element", "<p>This is a test</p>"); new Insertion.Bottom("element", "\074p\076This is a test\074/p\076");
new Insertion.Bottom("element", "<p>This is a test</p>");
["foo", "bar"].each(Element.remove); ["foo", "bar"].each(Element.remove);
Element.update("baz", "<p>This is a test</p>"); Element.update("baz", "\074p\076This is a test\074/p\076");)
EOS
assert_equal expected, @generator.to_s
end end
def test_element_access def test_element_access

View file

@ -1,3 +1,17 @@
*1.2.5* (October 12th, 2007)
* Depend on Action Pack 1.13.5
* Depend on Active Record 1.15.5
*1.2.4* (October 4th, 2007)
* Depend on Action Pack 1.13.4
* Depend on Active Record 1.15.4
*1.2.3* (March 12th, 2007) *1.2.3* (March 12th, 2007)
* Depend on Action Pack 1.13.3 * Depend on Action Pack 1.13.3

View file

@ -71,8 +71,8 @@ spec = Gem::Specification.new do |s|
s.rubyforge_project = "aws" s.rubyforge_project = "aws"
s.homepage = "http://www.rubyonrails.org" s.homepage = "http://www.rubyonrails.org"
s.add_dependency('actionpack', '= 1.13.3' + PKG_BUILD) s.add_dependency('actionpack', '= 1.13.5' + PKG_BUILD)
s.add_dependency('activerecord', '= 1.15.3' + PKG_BUILD) s.add_dependency('activerecord', '= 1.15.5' + PKG_BUILD)
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'

View file

@ -2,7 +2,7 @@ module ActionWebService
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 1 MAJOR = 1
MINOR = 2 MINOR = 2
TINY = 3 TINY = 5
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View file

@ -1,3 +1,37 @@
*1.15.5* (October 12th, 2007)
* Depend on Action Pack 1.4.4
*1.15.4* (October 4th, 2007)
* Fix #count on a has_many :through association so that it recognizes the :uniq option. Closes #8801 [lifofifo]
* Don't clobber includes passed to has_many.count [danger]
* Make sure has_many uses :include when counting [danger]
* Save associated records only if the association is already loaded. #8713 [blaine]
* Changing the :default Date format doesn't break date quoting. #6312 [bshand, Elias]
* Allow nil serialized attributes with a set class constraint. #7293 [sandofsky]
* belongs_to assignment creates a new proxy rather than modifying its target in-place. #8412 [mmangino@elevatedrails.com]
* Fix column type detection while loading fixtures. Closes #7987 [roderickvd]
* Document deep eager includes. #6267 [Josh Susser, Dan Manges]
* Oracle: extract column length for CHAR also. #7866 [ymendel]
* Small additions and fixes for ActiveRecord documentation. Closes #7342 [jeremymcanally]
* SQLite: binary escaping works with $KCODE='u'. #7862 [tsuka]
* Improved cloning performance by relying less on exception raising #8159 [Blaine]
*1.15.3* (March 12th, 2007) *1.15.3* (March 12th, 2007)
* Allow a polymorphic :source for has_many :through associations. Closes #7143 [protocool] * Allow a polymorphic :source for has_many :through associations. Closes #7143 [protocool]

View file

@ -151,7 +151,7 @@ spec = Gem::Specification.new do |s|
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) } s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
end end
s.add_dependency('activesupport', '= 1.4.2' + PKG_BUILD) s.add_dependency('activesupport', '= 1.4.4' + PKG_BUILD)
s.files.delete "test/fixtures/fixture_database.sqlite" s.files.delete "test/fixtures/fixture_database.sqlite"
s.files.delete "test/fixtures/fixture_database_2.sqlite" s.files.delete "test/fixtures/fixture_database_2.sqlite"

View file

@ -63,7 +63,7 @@ module ActiveRecord
#{scope_condition_method} #{scope_condition_method}
after_destroy :remove_from_list before_destroy :remove_from_list
before_create :add_to_list_bottom before_create :add_to_list_bottom
EOV EOV
end end
@ -74,6 +74,7 @@ module ActiveRecord
# lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return true if that chapter is # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return true if that chapter is
# the first in the list of all chapters. # the first in the list of all chapters.
module InstanceMethods module InstanceMethods
# Insert the item at the given position (defaults to the top position of 1).
def insert_at(position = 1) def insert_at(position = 1)
insert_at_position(position) insert_at_position(position)
end end
@ -118,8 +119,12 @@ module ActiveRecord
end end
end end
# Removes the item from the list.
def remove_from_list def remove_from_list
decrement_positions_on_lower_items if in_list? if in_list?
decrement_positions_on_lower_items
update_attribute position_column, nil
end
end end
# Increase the position of this item without adjusting the rest of the list. # Increase the position of this item without adjusting the rest of the list.
@ -162,6 +167,7 @@ module ActiveRecord
) )
end end
# Test if this record is in a list
def in_list? def in_list?
!send(position_column).nil? !send(position_column).nil?
end end
@ -178,21 +184,26 @@ module ActiveRecord
# Overwrite this method to define the scope of the list changes # Overwrite this method to define the scope of the list changes
def scope_condition() "1" end def scope_condition() "1" end
# Returns the bottom position number in the list.
# bottom_position_in_list # => 2
def bottom_position_in_list(except = nil) def bottom_position_in_list(except = nil)
item = bottom_item(except) item = bottom_item(except)
item ? item.send(position_column) : 0 item ? item.send(position_column) : 0
end end
# Returns the bottom item
def bottom_item(except = nil) def bottom_item(except = nil)
conditions = scope_condition conditions = scope_condition
conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC") acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
end end
# Forces item to assume the bottom position in the list.
def assume_bottom_position def assume_bottom_position
update_attribute(position_column, bottom_position_in_list(self).to_i + 1) update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
end end
# Forces item to assume the top position in the list.
def assume_top_position def assume_top_position
update_attribute(position_column, 1) update_attribute(position_column, 1)
end end
@ -227,6 +238,7 @@ module ActiveRecord
) )
end end
# Increments position (<tt>position_column</tt>) of all items in the list.
def increment_positions_on_all_items def increment_positions_on_all_items
acts_as_list_class.update_all( acts_as_list_class.update_all(
"#{position_column} = (#{position_column} + 1)", "#{scope_condition}" "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"

View file

@ -70,16 +70,23 @@ module ActiveRecord
nodes nodes
end end
# Returns the root node of the tree.
def root def root
node = self node = self
node = node.parent while node.parent node = node.parent while node.parent
node node
end end
# Returns all siblings of the current node.
#
# subchild1.siblings # => [subchild2]
def siblings def siblings
self_and_siblings - [self] self_and_siblings - [self]
end end
# Returns all siblings and a reference to the current node.
#
# subchild1.self_and_siblings # => [subchild1, subchild2]
def self_and_siblings def self_and_siblings
parent ? parent.children : self.class.roots parent ? parent.children : self.class.roots
end end

View file

@ -352,7 +352,15 @@ module ActiveRecord
# for post in Post.find(:all, :include => [ :author, :comments ]) # for post in Post.find(:all, :include => [ :author, :comments ])
# #
# That'll add another join along the lines of: LEFT OUTER JOIN comments ON comments.post_id = posts.id. And we'll be down to 1 query. # That'll add another join along the lines of: LEFT OUTER JOIN comments ON comments.post_id = posts.id. And we'll be down to 1 query.
# But that shouldn't fool you to think that you can pull out huge amounts of data with no performance penalty just because you've reduced #
# To include a deep hierarchy of associations, using a hash:
#
# for post in Post.find(:all, :include => [ :author, { :comments => { :author => :gravatar } } ])
#
# That'll grab not only all the comments but all their authors and gravatar pictures. You can mix and match
# symbols, arrays and hashes in any combination to describe the associations you want to load.
#
# All of this power shouldn't fool you into thinking that you can pull out huge amounts of data with no performance penalty just because you've reduced
# the number of queries. The database still needs to send all the data to Active Record and it still needs to be processed. So it's no # the number of queries. The database still needs to send all the data to Active Record and it still needs to be processed. So it's no
# catch-all for performance problems, but it's a great way to cut down on the number of queries in a situation as the one described above. # catch-all for performance problems, but it's a great way to cut down on the number of queries in a situation as the one described above.
# #
@ -734,6 +742,7 @@ module ActiveRecord
deprecated_association_comparison_method(reflection.name, reflection.class_name) deprecated_association_comparison_method(reflection.name, reflection.class_name)
end end
# Create the callbacks to update counter cache
if options[:counter_cache] if options[:counter_cache]
cache_column = options[:counter_cache] == true ? cache_column = options[:counter_cache] == true ?
"#{self.to_s.underscore.pluralize}_count" : "#{self.to_s.underscore.pluralize}_count" :
@ -871,6 +880,12 @@ module ActiveRecord
end end
private private
# Generate a join table name from two provided tables names.
# The order of names in join name is determined by lexical precedence.
# join_table_name("members", "clubs")
# => "clubs_members"
# join_table_name("members", "special_clubs")
# => "members_special_clubs"
def join_table_name(first_table_name, second_table_name) def join_table_name(first_table_name, second_table_name)
if first_table_name < second_table_name if first_table_name < second_table_name
join_table = "#{first_table_name}_#{second_table_name}" join_table = "#{first_table_name}_#{second_table_name}"
@ -901,7 +916,7 @@ module ActiveRecord
define_method("#{reflection.name}=") do |new_value| define_method("#{reflection.name}=") do |new_value|
association = instance_variable_get("@#{reflection.name}") association = instance_variable_get("@#{reflection.name}")
if association.nil? if association.nil? || association.target != new_value
association = association_proxy_class.new(self, reflection) association = association_proxy_class.new(self, reflection)
end end
@ -911,10 +926,7 @@ module ActiveRecord
instance_variable_set("@#{reflection.name}", association) instance_variable_set("@#{reflection.name}", association)
else else
instance_variable_set("@#{reflection.name}", nil) instance_variable_set("@#{reflection.name}", nil)
return nil
end end
association
end end
define_method("set_#{reflection.name}_target") do |target| define_method("set_#{reflection.name}_target") do |target|
@ -982,12 +994,15 @@ module ActiveRecord
after_callback = <<-end_eval after_callback = <<-end_eval
association = instance_variable_get("@#{association_name}") association = instance_variable_get("@#{association_name}")
if association.respond_to?(:loaded?) records_to_save = if @new_record_before_save
if @new_record_before_save association
records_to_save = association elsif association.respond_to?(:loaded?) && association.loaded?
association.select { |record| record.new_record? }
else else
records_to_save = association.select { |record| record.new_record? } []
end end
if !records_to_save.blank?
records_to_save.each { |record| association.send(:insert_record, record) } records_to_save.each { |record| association.send(:insert_record, record) }
association.send(:construct_sql) # reconstruct the SQL queries now that we know the owner's id association.send(:construct_sql) # reconstruct the SQL queries now that we know the owner's id
end end

View file

@ -91,7 +91,11 @@ module ActiveRecord
attributes.collect { |attr| create(attr) } attributes.collect { |attr| create(attr) }
else else
record = build(attributes) record = build(attributes)
record.save unless @owner.new_record? if @owner.new_record?
ActiveSupport::Deprecation.warn("Calling .create on a has_many association without saving its owner will not work in rails 2.0, you probably want .build instead")
else
record.save
end
record record
end end
end end

View file

@ -50,7 +50,7 @@ module ActiveRecord
options[:conditions] = options[:conditions].nil? ? options[:conditions] = options[:conditions].nil? ?
@finder_sql : @finder_sql :
@finder_sql + " AND (#{sanitize_sql(options[:conditions])})" @finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
options[:include] = @reflection.options[:include] options[:include] ||= @reflection.options[:include]
@reflection.klass.count(column_name, options) @reflection.klass.count(column_name, options)
end end
@ -138,7 +138,7 @@ module ActiveRecord
elsif @reflection.options[:counter_sql] elsif @reflection.options[:counter_sql]
@reflection.klass.count_by_sql(@counter_sql) @reflection.klass.count_by_sql(@counter_sql)
else else
@reflection.klass.count(:conditions => @counter_sql) @reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
end end
@target = [] and loaded if count == 0 @target = [] and loaded if count == 0

View file

@ -102,6 +102,16 @@ module ActiveRecord
calculate(:sum, *args, &block) calculate(:sum, *args, &block)
end end
def count(*args)
column_name, options = @reflection.klass.send(:construct_count_options_from_legacy_args, *args)
if @reflection.options[:uniq]
# This is needed becase 'SELECT count(DISTINCT *)..' is not valid sql statement.
column_name = "#{@reflection.klass.table_name}.#{@reflection.klass.primary_key}" if column_name == :all
options.merge!(:distinct => true)
end
@reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
end
protected protected
def method_missing(method, *args, &block) def method_missing(method, *args, &block)
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))

View file

@ -575,7 +575,7 @@ module ActiveRecord #:nodoc:
# Specifies that the attribute by the name of +attr_name+ should be serialized before saving to the database and unserialized # Specifies that the attribute by the name of +attr_name+ should be serialized before saving to the database and unserialized
# after loading from the database. The serialization is done through YAML. If +class_name+ is specified, the serialized # after loading from the database. The serialization is done through YAML. If +class_name+ is specified, the serialized
# object must be of that class on retrieval or +SerializationTypeMismatch+ will be raised. # object must be of that class on retrieval, or nil. Otherwise, +SerializationTypeMismatch+ will be raised.
def serialize(attr_name, class_name = Object) def serialize(attr_name, class_name = Object)
serialized_attributes[attr_name.to_s] = class_name serialized_attributes[attr_name.to_s] = class_name
end end
@ -1188,6 +1188,9 @@ module ActiveRecord #:nodoc:
# #
# It's even possible to use all the additional parameters to find. For example, the full interface for find_all_by_amount # It's even possible to use all the additional parameters to find. For example, the full interface for find_all_by_amount
# is actually find_all_by_amount(amount, options). # is actually find_all_by_amount(amount, options).
#
# This also enables you to initialize a record if it is not found, such as find_or_initialize_by_amount(amount)
# or find_or_create_by_user_and_password(user, password).
def method_missing(method_id, *arguments) def method_missing(method_id, *arguments)
if match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(method_id.to_s) if match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(method_id.to_s)
finder, deprecated_finder = determine_finder(match), determine_deprecated_finder(match) finder, deprecated_finder = determine_finder(match), determine_deprecated_finder(match)
@ -1957,7 +1960,7 @@ module ActiveRecord #:nodoc:
def unserialize_attribute(attr_name) def unserialize_attribute(attr_name)
unserialized_object = object_from_yaml(@attributes[attr_name]) unserialized_object = object_from_yaml(@attributes[attr_name])
if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
@attributes[attr_name] = unserialized_object @attributes[attr_name] = unserialized_object
else else
raise SerializationTypeMismatch, raise SerializationTypeMismatch,
@ -2156,7 +2159,13 @@ module ActiveRecord #:nodoc:
def clone_attribute_value(reader_method, attribute_name) def clone_attribute_value(reader_method, attribute_name)
value = send(reader_method, attribute_name) value = send(reader_method, attribute_name)
case value
when nil, Fixnum, true, false
value
else
value.clone value.clone
end
rescue TypeError, NoMethodError rescue TypeError, NoMethodError
value value
end end

View file

@ -242,8 +242,8 @@ module ActiveRecord
options.assert_valid_keys(CALCULATIONS_OPTIONS) options.assert_valid_keys(CALCULATIONS_OPTIONS)
end end
# converts a given key to the value that the database adapter returns as # Converts a given key to the value that the database adapter returns as
# # as a usable column name.
# users.id #=> users_id # users.id #=> users_id
# sum(id) #=> sum_id # sum(id) #=> sum_id
# count(distinct users.id) #=> count_distinct_users_id # count(distinct users.id) #=> count_distinct_users_id

View file

@ -24,7 +24,7 @@ module ActiveRecord
when Float, Fixnum, Bignum then value.to_s when Float, Fixnum, Bignum then value.to_s
# BigDecimals need to be output in a non-normalized form and quoted. # BigDecimals need to be output in a non-normalized form and quoted.
when BigDecimal then value.to_s('F') when BigDecimal then value.to_s('F')
when Date then "'#{value.to_s}'" when Date then "'#{value.to_s(:db)}'"
when Time, DateTime then "'#{quoted_date(value)}'" when Time, DateTime then "'#{quoted_date(value)}'"
else "'#{quote_string(value.to_yaml)}'" else "'#{quote_string(value.to_yaml)}'"
end end

View file

@ -320,6 +320,7 @@ begin
decode(data_type, 'NUMBER', data_precision, decode(data_type, 'NUMBER', data_precision,
'FLOAT', data_precision, 'FLOAT', data_precision,
'VARCHAR2', data_length, 'VARCHAR2', data_length,
'CHAR', data_length,
null) as limit, null) as limit,
decode(data_type, 'NUMBER', data_scale, null) as scale decode(data_type, 'NUMBER', data_scale, null) as scale
from all_tab_columns from all_tab_columns

View file

@ -68,7 +68,7 @@ module ActiveRecord
class SQLiteColumn < Column #:nodoc: class SQLiteColumn < Column #:nodoc:
class << self class << self
def string_to_binary(value) def string_to_binary(value)
value.gsub(/\0|\%/) do |b| value.gsub(/\0|\%/n) do |b|
case b case b
when "\0" then "%00" when "\0" then "%00"
when "%" then "%25" when "%" then "%25"
@ -77,7 +77,7 @@ module ActiveRecord
end end
def binary_to_string(value) def binary_to_string(value)
value.gsub(/%00|%25/) do |b| value.gsub(/%00|%25/n) do |b|
case b case b
when "%00" then "\0" when "%00" then "\0"
when "%25" then "%" when "%25" then "%"

View file

@ -1,7 +1,7 @@
module ActiveRecord module ActiveRecord
class Base class Base
class << self class << self
# This method is deprecated in favor of find with the :conditions option. # DEPRECATION NOTICE: This method is deprecated in favor of find with the :conditions option.
# #
# Works like find, but the record matching +id+ must also meet the +conditions+. # Works like find, but the record matching +id+ must also meet the +conditions+.
# +RecordNotFound+ is raised if no record can be found matching the +id+ or meeting the condition. # +RecordNotFound+ is raised if no record can be found matching the +id+ or meeting the condition.
@ -12,7 +12,7 @@ module ActiveRecord
end end
deprecate :find_on_conditions => "use find(ids, :conditions => conditions)" deprecate :find_on_conditions => "use find(ids, :conditions => conditions)"
# This method is deprecated in favor of find(:first, options). # DEPRECATION NOTICE: This method is deprecated in favor of find(:first, options).
# #
# Returns the object for the first record responding to the conditions in +conditions+, # Returns the object for the first record responding to the conditions in +conditions+,
# such as "group = 'master'". If more than one record is returned from the query, it's the first that'll # such as "group = 'master'". If more than one record is returned from the query, it's the first that'll
@ -24,7 +24,7 @@ module ActiveRecord
end end
deprecate :find_first => "use find(:first, ...)" deprecate :find_first => "use find(:first, ...)"
# This method is deprecated in favor of find(:all, options). # DEPRECATION NOTICE: This method is deprecated in favor of find(:all, options).
# #
# Returns an array of all the objects that could be instantiated from the associated # Returns an array of all the objects that could be instantiated from the associated
# table in the database. The +conditions+ can be used to narrow the selection of objects (WHERE-part), # table in the database. The +conditions+ can be used to narrow the selection of objects (WHERE-part),

View file

@ -412,7 +412,7 @@ class Fixture #:nodoc:
klass = @class_name.constantize rescue nil klass = @class_name.constantize rescue nil
list = @fixture.inject([]) do |fixtures, (key, value)| list = @fixture.inject([]) do |fixtures, (key, value)|
col = klass.columns_hash[key] if klass.kind_of?(ActiveRecord::Base) col = klass.columns_hash[key] if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
fixtures << ActiveRecord::Base.connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r") fixtures << ActiveRecord::Base.connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
end end
list * ', ' list * ', '

View file

@ -5,15 +5,6 @@ module ActiveRecord
# Timestamping can be turned off by setting # Timestamping can be turned off by setting
# <tt>ActiveRecord::Base.record_timestamps = false</tt> # <tt>ActiveRecord::Base.record_timestamps = false</tt>
# #
# Keep in mind that, via inheritance, you can turn off timestamps on a per
# model basis by setting <tt>record_timestamps</tt> to false in the desired
# models.
#
# class Feed < ActiveRecord::Base
# self.record_timestamps = false
# # ...
# end
#
# Timestamps are in the local timezone by default but can use UTC by setting # Timestamps are in the local timezone by default but can use UTC by setting
# <tt>ActiveRecord::Base.default_timezone = :utc</tt> # <tt>ActiveRecord::Base.default_timezone = :utc</tt>
module Timestamp module Timestamp

View file

@ -2,7 +2,7 @@ module ActiveRecord
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 1 MAJOR = 1
MINOR = 15 MINOR = 15
TINY = 3 TINY = 5
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View file

@ -169,6 +169,12 @@ class EagerAssociationTest < Test::Unit::TestCase
assert_equal 0, posts.size assert_equal 0, posts.size
end end
def test_eager_count_performed_on_a_has_many_association_with_multi_table_conditional
author = authors(:david)
author_posts_without_comments = author.posts.select { |post| post.comments.blank? }
assert_equal author_posts_without_comments.size, author.posts.count(:all, :include => :comments, :conditions => 'comments.id is null')
end
def test_eager_with_has_and_belongs_to_many_and_limit def test_eager_with_has_and_belongs_to_many_and_limit
posts = Post.find(:all, :include => :categories, :order => "posts.id", :limit => 3) posts = Post.find(:all, :include => :categories, :order => "posts.id", :limit => 3)
assert_equal 3, posts.size assert_equal 3, posts.size
@ -272,6 +278,13 @@ class EagerAssociationTest < Test::Unit::TestCase
assert_equal companies(:first_firm, :reload).account, f.account assert_equal companies(:first_firm, :reload).account, f.account
end end
def test_eager_with_multi_table_conditional_properly_counts_the_records_when_using_size
author = authors(:david)
posts_with_no_comments = author.posts.select { |post| post.comments.blank? }
assert_equal posts_with_no_comments.size, author.posts_with_no_comments.size
assert_equal posts_with_no_comments, author.posts_with_no_comments
end
def test_eager_with_invalid_association_reference def test_eager_with_invalid_association_reference
assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=> :monkeys ) post = Post.find(6, :include=> :monkeys )

View file

@ -30,6 +30,15 @@ class AssociationsJoinModelTest < Test::Unit::TestCase
assert_equal 1, authors(:mary).unique_categorized_posts.size assert_equal 1, authors(:mary).unique_categorized_posts.size
end end
def test_has_many_uniq_through_count
author = authors(:mary)
assert !authors(:mary).unique_categorized_posts.loaded?
assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count }
assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count(:title, {}) }
assert_queries(1) { assert_equal 0, author.unique_categorized_posts.count(:title, { :conditions => "title is NULL" }) }
assert !authors(:mary).unique_categorized_posts.loaded?
end
def test_polymorphic_has_many def test_polymorphic_has_many
assert posts(:welcome).taggings.include?(taggings(:welcome_general)) assert posts(:welcome).taggings.include?(taggings(:welcome_general))
end end

View file

@ -67,7 +67,7 @@ class AssociationsTest < Test::Unit::TestCase
end end
class AssociationProxyTest < Test::Unit::TestCase class AssociationProxyTest < Test::Unit::TestCase
fixtures :authors, :posts fixtures :authors, :posts, :developers, :projects, :developers_projects
def test_proxy_accessors def test_proxy_accessors
welcome = posts(:welcome) welcome = posts(:welcome)
@ -87,6 +87,19 @@ class AssociationProxyTest < Test::Unit::TestCase
david.posts_with_extension.first # force load target david.posts_with_extension.first # force load target
assert_equal david.posts_with_extension, david.posts_with_extension.testing_proxy_target assert_equal david.posts_with_extension, david.posts_with_extension.testing_proxy_target
end end
def test_save_on_parent_does_not_load_target
david = developers(:david)
assert !david.projects.loaded?
david.update_attribute(:created_at, Time.now)
assert !david.projects.loaded?
end
def test_save_on_parent_saves_children
developer = Developer.create :name => "Bryan", :salary => 50_000
assert_equal 1, developer.reload.audit_logs.size
end
end end
class HasOneAssociationsTest < Test::Unit::TestCase class HasOneAssociationsTest < Test::Unit::TestCase
@ -583,6 +596,13 @@ class HasManyAssociationsTest < Test::Unit::TestCase
assert_equal 3, first_firm.plain_clients.size assert_equal 3, first_firm.plain_clients.size
end end
def test_regular_create_on_has_many_when_parent_is_new_raises
assert_deprecated(/.build instead/) do
firm = Firm.new
firm.plain_clients.create :name=>"Whoever"
end
end
def test_adding_a_mismatch_class def test_adding_a_mismatch_class
assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << nil } assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << nil }
assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << 1 } assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << 1 }
@ -1008,6 +1028,19 @@ class BelongsToAssociationsTest < Test::Unit::TestCase
assert_equal apple.id, citibank.firm_id assert_equal apple.id, citibank.firm_id
end end
def test_no_unexpected_aliasing
first_firm = companies(:first_firm)
another_firm = companies(:another_firm)
citibank = Account.create("credit_limit" => 10)
citibank.firm = first_firm
original_proxy = citibank.firm
citibank.firm = another_firm
assert_equal first_firm.object_id, original_proxy.object_id
assert_equal another_firm.object_id, citibank.firm.object_id
end
def test_creating_the_belonging_object def test_creating_the_belonging_object
citibank = Account.create("credit_limit" => 10) citibank = Account.create("credit_limit" => 10)
apple = citibank.create_firm("name" => "Apple") apple = citibank.create_firm("name" => "Apple")

View file

@ -1086,16 +1086,29 @@ class BasicsTest < Test::Unit::TestCase
assert_equal(myobj, topic.content) assert_equal(myobj, topic.content)
end end
def test_serialized_attribute_with_class_constraint def test_nil_serialized_attribute_with_class_constraint
myobj = MyObject.new('value1', 'value2') myobj = MyObject.new('value1', 'value2')
topic = Topic.create("content" => myobj) topic = Topic.new
assert_nil topic.content
end
def test_should_raise_exception_on_serialized_attribute_with_type_mismatch
myobj = MyObject.new('value1', 'value2')
topic = Topic.new(:content => myobj)
assert topic.save
Topic.serialize(:content, Hash) Topic.serialize(:content, Hash)
assert_raise(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).content } assert_raise(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).content }
ensure
Topic.serialize(:content)
end
def test_serialized_attribute_with_class_constraint
settings = { "color" => "blue" } settings = { "color" => "blue" }
Topic.find(topic.id).update_attribute("content", settings) Topic.serialize(:content, Hash)
topic = Topic.new(:content => settings)
assert topic.save
assert_equal(settings, Topic.find(topic.id).content) assert_equal(settings, Topic.find(topic.id).content)
ensure
Topic.serialize(:content) Topic.serialize(:content)
end end

View file

@ -25,6 +25,7 @@ class Author < ActiveRecord::Base
has_many :hello_posts, :class_name => "Post", :conditions => "posts.body = 'hello'" has_many :hello_posts, :class_name => "Post", :conditions => "posts.body = 'hello'"
has_many :hello_post_comments, :through => :hello_posts, :source => :comments has_many :hello_post_comments, :through => :hello_posts, :source => :comments
has_many :posts_with_no_comments, :class_name => 'Post', :conditions => 'comments.id is null', :include => :comments
has_many :other_posts, :class_name => "Post" has_many :other_posts, :class_name => "Post"
has_many :posts_with_callbacks, :class_name => "Post", :before_add => :log_before_adding, has_many :posts_with_callbacks, :class_name => "Post", :before_add => :log_before_adding,

View file

@ -0,0 +1,437 @@
flowers:
id: 1
data: !binary | /9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsL
DBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/
2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy
MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAFeAQcDASIAAhEBAxEB/8QA
HAAAAgMBAQEBAAAAAAAAAAAAAAUDBAYCBwEI/8QARRAAAgEDAwIEAwYEBAQD
CAMAAQIDAAQRBRIhMUEGE1FhInGBFDKRobHBFSNC0SRSYuEzcvDxBxZDJTRT
Y3OCkqJEstL/xAAaAQADAQEBAQAAAAAAAAAAAAAAAgMBBAUG/8QAKxEAAgIC
AgICAgIBBAMAAAAAAAECEQMhEjEEQRNRIjIFYaEUI0KBcZGx/9oADAMBAAIR
AxEAPwD3+iiigAooooAKKKKACiiigAooqrf6haaZaPdXtxHBCvV3OPoPU+1A
Fqo5p4reF5ZnVI0BZmY4AApBp/iy01qK5fT9wWCXy3MgwegIOPQ57+lSGXz9
xL+Z2POaFsOhbe/+JGjW0hSBZ7kD/wBREwn4nk/QVHD4zfUELWzRqMf0jOPn
np+Ffbzw9otxG7T2MKZyWkT4D88ilGn6XZ6e0v2SNgjH/iSEFmHYewqU7Xsd
UxjNqd9Nz9plAPUD/aq+ZmU+bI7c9yTx+NSrt5AxnviuRgMQSfnUmxkivMko
t5PIYGbafL3DI3Y4zUGnXBtbVbZrlzcRAmXbkc5ycc+9XWkQA5PGPqKzng+x
OqmZpmfDO3mMDghc8YPrWxV6Rr6NLZ6/dOFaG9kZTnaM7s44PWjUfHl5ozQm
4s1uInJBYZQ59KzFpaz6Fqs2kytlQ++3du+en4jj51Y8QvDcaUF3Av56ADHI
OcHP40OUogops9GtPEllcBRIWgdh0ccfiKbI6yKGRgynoQcg151tA4AzirFt
c3Fo5e2mdD3CnIPzFOsn2K4fRv6Kztl4ojICX6GJu8iqdv8AtT+KWOaNZInV
0YZVlOQaqmn0I1R3RRRWmBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAU
UUi1TWGBaC1J9GlH6D+9Y3QJFrUdYisgUQCSb/LnhfnWYeBfEE8v8Sj86Jei
MeAe2PTv0qIqWVsTMnvj+9TQyvBbCKCRNx5aRxyT8qTnuxqMdqWnyeCtTeS0
eRtM1H4MMcmNwcgZ79Tg+nyrR+FtU+1zy27cMi5bJ4znioNc0yTWNPmgciR2
X4GJ+6w6Y9P96W+DUVvD6nawnSZ1kcH4gwPGT8jSXux2rRo9WklbVltpR/h5
YMR55BIOScfhR5SEbhkgjpnNRX0811bwBgBcRSh0ZiACOhB7ZIohiRQYymNp
wTtFLJ7BdEhQ46DjoOlLdUj1CSPFrK0L9Q+0FfkfY+vamRBAciQr74Jrpjv2
MsilCMjjOfrWaNsWeH7HUrI3V9q7KqrATCcq3xd/u+1MvBaQjw/HJEMu7MJP
mGP96W6nqZ0u1/lyfAzZwrdKe+HpI5dEhljQIrs7kdsljmqQq9Cyeip4p0aT
VLeKe1QNdwdADguvpn58/jWI1CDW4PLF7Yy7XkGP5YG9+wyOpzXqu7r/AN6y
Hi2/JvtLsYQMi6V3bBOCBkDHyOa3LFVbNxSfIpaWdTUN/ESEkP3YdmCo9Wx0
z2FNt27+lh/y1BHE6AkXBLMcneepqdGcjDKp9xUn/RpIFBGSWJ/1VNZXFzpU
7T2h8yJzmS3HRvceh/WuN4AyQT7CoZ5YUTzHLY6AhelCdGNG407U7bU7cTW7
gjuO4PpVystpyC2tIlUkFRnPQ5praa5aTXYs2mX7R6DofbPr7V0p/ZIaUUUV
oBRRRQAUUUUAFFFFABRRRQAUUUUAFFFI9b1IBHtYnKnGJHXkj2FY3QIj1bVt
7m2t3wv9bjv7fKkgCqBt49s4rvChVAIwR3FUri+t4AwBDt/lU5IqDdlEi0wb
qGP4igDoeM+uKV/xiIuCLZsgfeBFTJq0Dbd4kjJ7EVlm0XfLywOyPPqc1m7K
Qaf4tvbBXDxXq/aYmRsDcPvD5itCk0cikqSV9uaw3ieC60nUbXWbVH2QShg/
YZ7Ee/SjsaPdG6+PaeXHGPi+Kvq4XOSSf+XioYb6G5tVnWaMb0V1G7Gc88Zq
YljkDI49jWCg5B3Kccjngj86TPdXGkTJ5+JrVzxL/f0b3/q+dOdzc9Bx3FVp
XcBo7iHfGwwXjG4YPqvX9ankjf8ATArx2tvq9wpuIle2lJhPzIyrexzVrwoy
2PhlIp3OYJ5YyzHJOHNZ+DUP4XDJaKA/xCe3kU8DBG5T+Rx2qTSn82/1JpN2
EuCVbkD4iScDt2rcWZPXsKtM1UWvW0k5jZZI1z8LshAP/X0pTqNlbfx+G5uJ
WFrnz12ty79MZqxEMHImb/8AL+9KNatXu7zSLYOcS3JQ4OMjGccfKquV9mRV
PQ7VgzHZh0z8J9R2r4ygjJgOR6GvgiMfwFcY4x6e1SKcZwG49KUY+bR03MPY
io5UeRo18xVj3DcGA6Z55qYnjJPHuKjCuZnYrwcbfzzWICxe3c038u2bZH3f
puHtzxUEcSx4KiFccg4qfZnBKr060eWrf+mh96ZuxaNDpGqmcCC4dfN/oYH7
3+9OKw2NoBUKMc5BrS6TqX2uLypionUev3h61WE70xWvY0oooqgoUUUUAFFF
FABRRRQAUUVDd3Mdnay3Epwkaljjv7UALNe12DR4UViTNLwoXBKjuxrINrtp
8Z/mk5yOBzz86w2v22q61rN5qV1ckFz8EYHCL/So9gPxOaZeEfCsGsWly1/N
KskUgVfLAAwR3yDUv3einFJWxzda1PczR2lhbu80owoIGc/sPepLfwO8+ZdU
1CRnbny4Oi/U9fwp7o3h+y0NZfs293kxueTG7HoMdqamnUUhXL6MqfAmnf03
V2Pqp/aqdz4OvrYM9hqmVAJ2Tqf2yK2hrlxlGHqD+lDimHJnnUv22ythcDEx
ABIjBRjn0x1qP+Pw3VpNZXnmbJVKOlwCSAfzpyozaRkf6TWquLa3ucieCKYf
/MQN+tShHkikpJHleo3yfaLNIifLREjXYODgY4rZ2+pQGBRL8L4GcqcE/Okf
iHw7Y2/i/RpREYbG9cwSrExUB8cEemcj8Kin8PxRW0vkyTRshZQwkORzjPvW
SVM1U1Zp1vrZwwEij/7hXYuoC6YmTnpzSlvBN1jCa25/5oz/AHqH/wAl6n0/
jEeP+RqPjkZcfs41y0S9VpIDCJoviLAYPPTJ7qfun0OD2r54Xt2tNJkkmcMJ
ZN2ScEEDBBqm2lXlhLceZfebLGTFjACyBh9315qHSI7vUGuNLtZ1Ty084tJ0
zkcHg+p/Co8ayX7D0bBJ4eAJFz6Ag0n1m8W3v9Ikt23SpdhsenAGPzquPC+u
MuUvrNvQ5P8A/mld5puoQ6nBZ3MqNPuDI0S5A4zjGOapJNKxoU2bjz4ppjIH
XLEnG6uvNjDEGVAfQsKyMPh/X5YopTJahZCQm4bTnnqMcdDVoeG/EQXn7ER6
7v8Aam4SYujSNcQooZnUAjgk1Vk1e1jbClpOOqAnFIBoV7BfCK8u1Zdm7bb/
AAjr3OM9qt2nhG2u1aee9vNpYgRq/Ax7nJpVFt0DaSsmk19UwRGqjHR2AxVV
vEvnSeTCUaTH3IlLn9KbweEtEhOfsXmn1lkZ/wB64nsLS21BBbW0UIEYyI0A
zyf7Uzx0rFUk2Imv9SuHKW+nXcjA87VAH1xU0C+J4LlLmDTJEkRtykuOD+Na
TRRiCZvWT9hTPNNCCasxyp0PtJvpNQ06KeaBrecjEsLHOxu/09KvVnLK7NrO
CfuNw4/etECCAQcg1YQ+0UUUAFFFFABRRRQAVjfFl7Jc3CWMLqIozmTJ6t2H
0rU31wLWymm3BSqEgnpntXmbxRFi93eFpGOWII6nr61LLKlQ8Fuylc282Sm9
GLZxtPFPfBltJBZ3byDDPMBjOeg6/nSaaGzyT9rcducf25p14Tt1gF5i5EpY
r8I4wOeaTF2PPo0lfK+18roInyuTzx619Nc5yR86AMmn/uij0X+9arOe9Zcg
Lan/AJW/U1pVbKA+oH6VHF7KZPRl/wDxAguZPDyXFvn/AAs6zPhckAdG9sHr
7Gu9NUa1pSz+YiCdd+SCeT1H41opESWN45FDRuCrKehB4IrzPStU/wDL+o3O
jzo0qwTlYOvIzwMDrng0ZNbGhuNL0enhgVBB4r4XGcZ57CobdmNuhZNjEZ25
6Z5xSme41FNajxbM9sgyxDBQFIIyBnLHPb2qquhIxuxdrM9iLq4tp7nE87/B
Eg+LPGPl659qVWd0ItanAPlSXVtASPUgDP49aZ3sS6hqmNnCTGSKXaTzxkYH
Oec4r5B/Cv4zJY+WJSYUy8icRhST97qM59ulQS5y0WStaNFo6sYQsz4y/Xpx
Vu50/SrbVTfyI5uUUlGZshBjGR/ekg1CQ38sUdxCI4uGVyCu3b14yevH071F
q2tm40O5ayQz7I2SRZOGMeSu/wBcZHz71alFD4sDlNL7HR1iC7ZYbWZJZFYl
gxycd+frXUM4eC423SlUDFnEmCp78Y4rxWzvrmyuT5UjJvDAc9Djg/iBWi0G
z1OWC7nvUxb3iZCyyhDLk5OBnv61kZNns5vAWJfsb5pUcIkzpdIcslwnBx6Z
qzDbm1i8vOVDEg56gms/a3t19maNIII41ciBUbKP/pO3gZ9+9ffD+uS3IuRf
xSQyrhREImyOee1brs4s/hucXKPo0OaUXuTqDsD9xRn8P96ak4AIIKsMg+tK
ZmBnum684/IClydHkxTTplvSRtsz7uf2phmqGncWae5J/OreaaHSFl2SZp1p
F1vjNu5+JOV9xSLNSQzNbzJKucqc/OmMNbRXEUiyxLIhyrDIrug0KKKKACii
uJHEcbOegGaAFOtOssE0LKXRY23KOp4rz4G2KqYrXzSB1MhOfpW5nlKpJKSM
gFufWkDFY4wcqD7cCo5R4GflePlm01Rjvg048JvbNNe+RC6nCbmL7gOvH71z
NMh/rQY5zmrOgXkbXU0COjBl3jaO4/2NLjVMeTtGgr4aK+E10ET4a5/qHzoJ
rnuOO9BhmSMwMPdx+Zp9A262iPqin8qRngEf63H503tDmygP+gfpUcXsrk6R
P8ZcgIdgXcX7DnpXnOvW/keMLO8x/LndkJxxuGQP1FbXVrxba2IbcQ3BCnBG
eAfx4+tYi91G+sFa4liaWzyFwy8MT0x6H3olttfQ2FPtG5nvrO3s42uyjKdg
2k45OPz6UtvJ7OzX+L3N/cSxN9yPHwEYxjH9Rz3+VUNW1C1bVbDTZraV5cBg
XJ2o3RenJ619n1C+kv7iyfS47q2ihDLC2SgbdgLkDt6VXotGHFHwv5MkGr6f
bsIGhMkqI5Ujg4O2ki+JGu5YtS+yC2aCRlLA7lZyh27vUdevv0qbSLkskpuo
pPMM3mJAchdmcFc91yzD5AVNJFDZ6vYXbqYrFpzIYHA2IdgIJ9Tlu/SpKVp+
jE1xaS2z7JPeaQl5qi6Oi6nJI3ltC2+IjIySOpx+fFfbLULmQLe3Vu9tLOrB
9q/CA3XI/PGO9T3Op2sV5HZlJWi6ZRMoueSQ3TH9qQ313cNrU9n9saa3SMPF
txg7jjHAHvTN0jVKUUnI+ar4euINIjlijE0GTK95GRgbuAo74+feov8AzXdJ
bSpcxx+RFCqCIjKS9sN36VY0zV5U8QyjyjeWUluYZLYOQriNfh/Aj86+eIbO
C70e3uY9GuLK6mw00IRjEqrkb1PoSRxSxaltHteL5X+phU4/lfY78MapBa6A
2pzLsWMsiW0RZsDspJJyOc57Yqg7R6nplrqtoLiR7TepG7bvVWyOexwevtXS
aWb3w3Y2lpfPGtunnO0cfwM56Bj1yMdKjtLN9QsorC31FLXVbdyksUHwM68H
cezH3HrzTd6FjlhHI3e7/wAG40mXULvR47i+hjiZmyio3RT0HrVCUh4p27lj
+pq1ptpLp1p5U15c3Ug+887Zx7AdBVILnT2bONxFLk6SPFzOMsspR6GlkNtn
EP8ATVjNQwDEEY9FH6VLVV0crOs0ZrnNFaYONC1KN55dPLfzI1EijPY9f2/G
n1eeQXMdv4hjvAnxxfy2YHqvf9a9CUhgCDkHoaVOxmqPtFFFMYFUdRkxGsY6
scn5VepPev5l03ovAoAWaqC2lXIBwdmc/UVmryATxqzyMSnI9K10iCWJ42+6
6lT9ayYsL74lLxoBkZLdcGo5NbHhspSbUhZAMFl6188NqV8RR7XLKUfPHtU0
+n3TDHn2pOO7Hp+FWPDGnyxarPcTPHlEwqo2c56n6Y/Olg7Y8lSZq818Nfa5
NXInJNcE81xdTpbW0kz/AHUGfn7VW068+22KTH72SrfMH/tWclfEwVOOZP8A
6rfrTKzcCwh3EDjH5ml03Dze0zfvVq2KHTB5jBVUsMk9OTSYV+VM6eKlSYv8
Szo1lJFjEoUlM9++PkQOD6rWSl1GPVNIudPQZUWrPvP+ZQCAPw/On+txfb9M
YRuXVVOwjnHr/wBfKs5a+H2to5J5b3EiwvlFwEAIPWun4JKTr2j0MfjPEnH7
Rb0O/TVbaF5IVDxIsQCgu74/PmtNNbyrbrcOVgeNcrGAM8c7iQOG9ecdqwXh
C5u43kitI1aTf1ILdR6d69Dgg1JmC3dygk67Qq/sKEvwSbJZE1BJtJCm0vG1
HR/Pmgt/tQm3w7i0bZHTnHIx17VT8TX88Wgwy3VpayyzTlFkWTcsLDuBjvjk
GpdUtG1LTHhglkV5JiiKATg55OR0yfeszqpKrp2nXVyzRjYJVPCnLEFh+PWo
T2miM0q19lvw/BqF/EkvkwSQxRsyksUDM2AARyDyDx061SuLhY9RR12hnQxy
NjlcZA/DJrVXFvarexWAypnjEVwqNtVWjH3/AJlcge/NYeVHinEbv8LMcM/U
Zri8mMqTT6ORt9n2y1aXQbthp7lrlsoFMQfqfetT4Wn1t9bmk1VtRvLdQd4+
0ZjVj2IzgnHbt6VVh0qS1msYI7VZprtVlcn4OMnI389AM1usGCwYFI02ofhj
GFHHaqeMr2bGTXQuvb43asQqRxIpWNEHAH96R3+iTW+u21/aIWNwoJKttZWC
jODVzdi3b3WtKijy48j7oBHscVbHtuy6yvHKyNmYaczu4Z/LOT747+9U8YsU
X3/TirWoEixkx7fqKqghhAB3GcfM1mTtEou7Y2XhQPQV1XzNFWJH2vvp865/
OvvagDPJMftcwbqHbr869C8O3n2zSY8nLxfy2+nT8sV57e2E/wDF5DAUMb4Y
5bGCeorUeEVubW6mhm27JUBGGz8Q/wBjUouplGribCiiirEzmRtkbN6AmkJJ
JJPU02v322rD/MQKUUAHz6VjdunjcXu5nBJwPQdhWz71kbkP9olC6Ygk3nJ2
gjOe1SylMfZQlGkYJM0wHrkCrPhj7AmtSfZ5pWeSIhEbtg5JriUTkfFpkBI9
QKseHZJP4vIg06OBfLPmSgY+QH1qWP8AYpL9TVk1H5ikZBB+X71WvZpIwCF/
l7SxORx8x1rLa54peztU8pfizxnvjviunrs6PH8KWT/sZ6/M8toIwAESTdNh
uQuPhOO/Jr54YdZLC5aMlohOVVuxwADj615/b69qeoaq08cbvb7fLuFUZBQk
9flk16fZoLfSrWK3gwFjVUQHAUY6n9frUlivLzsn5XirDNJPsXTn+bdA/wDx
jj86U6jeJb2RkuHxbxSt1PUk54Hc1b1HU4rS5ePyWnk35l8vlV/ufYfM1gvE
Mlzf6jHCC6yP8axFs+Wp7kdASPyrqxYuDcrs7/GwcGpSHV7eX1xobTWsQt0L
qI92S7ZOOnQUvk8OXklh5z3Msmw75IT0cdz8/nUHiDxMp0dLC1zHeJMjMwxh
dvPB789qjt/H9yLUR3dmjybcebE2AfmP7VWWXE7Q2by8TvGXtIvZdJ1PUHjb
Ys6xMpx3yeR+dX9S8V3ul30BCRSrMDnepLAdBgjpnmvPodUuWnlkZgzMMYbk
Ae1fBqtw95FPI+4xgKN3IA+tcDuzyJytJ/Z6SPFlvNA0UsJhZhjGf0980jPk
ag80TyZuG5LNzypzgew9PnVFmaRhNLAPL3KOExgk1Sllc3SNajE6uWPP3vlW
OaUqZabg40ns1uma1Dp97MsyBlZCvxLkx89P+WkV8Gv7u6nRT5QYFQzYIU8D
A79K6Zxd3Pm4I2oRIcfqKl0x7h7hzHCsqKdrsFB2Z7j9allleokZf7m6IPJ1
C5svtZaRre3HlKwbO056e3BNbDTNf+1aJJDcuBOIgiHu/b9vzpVc3FzHYtM8
3mxlzDcqMfyjxgkf5T2P71Rj8tdRRQ/lqM4A6ZPr7VBuWOaUfZFKmaHP8nHr
WpR1bhTyvBBHIrPTLZWUP8y5EwxztRjj8BUN3fXlpGl2pE0BAwzPyAfQ16sM
HFNyZ6P+kc1bdD7UHxbquOWccH6moxGRcQsP+GQAPxrODxDNdvkBYbdOMdSx
P5AVqbDElnFksSGzyfQ0QxxyxckZHxv9pyLwr7XxSDnBBIri4nW2haR8kDgK
OSx7Ae5pG67PPao73DdtyN2M49q6HSq9rC6K0kxBnkIL46D0Uew/uameWOLb
vdV3HCgnk/Kl5atgI7pYm1OZjfSRfFgoCODjmmmmOltdwT/xF22MMqSMEd6V
3Em3U5vMsDKSxG4KDkDpV5WhdCG01hn/AOVmot/kXS0ejCiqumzi5063mAI3
RjgjBorpIEGpt/w0+ZpfVvUGzdY/yqBVSgD4x2qT6DNI5ZY+GduW5zinbruR
l9QRWNuYjIqB5HAUYwvFSyDwL8k0KoWDA49aqWuoKuoRCMln5Coq5ySMc+1U
nVUTqfTJr5oK2y+IIQ80kbPlEZT1J7H2NThTkWj3shv9Qvopjc3AYRnekqbc
lSCRkEHHpS/WbqC/8LQ3ChfMViOgznJHz9K2uuae08EhtZo4lwfMSRcqw+Ve
fT2V5cyNZwNb+TGcyeQmFz8zkn3roPfw5ccoprTRBpvlWNgFnIMcoO/y/vxt
1Vx649PpW3i1K8ufDdobBV81l2NJnhSOoX3wM5PCjk+lefajpuo6YVuWhdrK
U8SgcBuhU+nPrTDTNVl03T7hHybeYEBCeG6ZGe3zrljOUMr5KzxJRyZPIbSv
ZFc6mJppXVi6hyikfdwP8vtyeTyaQajqN1Cs1nGw8mSTzS45cnbjBbrjHGKc
XiFUEtr/AC4CcALyDnBz6+tZ67tZBG8oRtkbBHJ7Ek4/HBp8kZqbk3pjediy
4Zu32KpG7CmOiaeZdQtZLy3ZrJ5Bvy20MM4HPpnFUZAf8hP0q3c6o8oYxIY5
GTBYHABxzgDpWxqjghXs0WsWFjNExgtYbW4iB4hXaGXuCPUdc1jAp3EGnEuv
S3LDKhJiMNj7p46/OoJNJuYLCLUiUa3kYrlWyyn3H0plrseauCaLFhcXETrF
5itHwWVxkEjp9acwaXHdSiZLYOqMCQJMLk9Mhsj86zkEzSXGegGWxn0FNLa+
lis5JLa8mt7gDbIqNgSxnr0689q55wuVoxPpmgtLOWS2lZmJUEMzyOAAfcjq
Dg8DPNPrS6g1K6ktbOwhgKBVy6/CMcsB3yKx2l6sttcLIYhcKse0RkZIOM7g
PbmnOlX0mpaVObdtupQnzBI7qo29CqjOT79uRW4fxSXstFpvfbDU7Bk1nzLj
NnEQBNKULRHPQE9s4pVLmO7lERQ/GdpDZBAx0PpTfxDLq1n4Wij1LUIj5s2y
SEgGRl6g5HYd/pzWbtUjhs2mWXzGX4uOMe36UZqg7JypScWO4/EFrCBHcpcM
/XK4wD8s1WkvjqtwsNu7NGWwqHjBPcis5d3G+Tdg5bOCO9QwTSQyrKjlHU5B
HY1s5znHi3oZ+TOS4y6PQL/TvIgh8pcPCNpH+cHrWgiuxaWSiVxGoxuJPTPQ
f7VlbDXHlRUuI2AIBV+uTjn61dtJI2kE8rEuGzDGxGB/qYk4B9M125fKxwin
jPanmgsaUTV298DdLAu2OJI2ll3n4lXgLn0J5OOwFVrzVIoWFzIQ0i/+72+e
Vz/6jDsT2z0HuazyWr3F35twpVmJkLQANxn1z0HSrwg0tzsjuZkctktLHnHu
Tj8q4ZvLNNw/yeRkwy3OySzvr26BiSRzvbLMik4HpTi3tPs264MZaVQT5kzc
jjsBk/iahtg8cax2WpxzKD8SqieZj2z1q9LaSNbyhru4clGwMqAePQCp4MDW
5ttnOL4r6OM7pGxnk59aaRX8Lx5WQHA7VmIPKePDBSetMkWBcbBtx3U4q10W
aN34dvEutPbY27y3K/jz+9FJ/Bjqs17EHLbgr8/Uf2oroi7RF9jS7O67lPvi
oKkmO6Zz6sajpjArJ63E9pNNKCrQFwPvEMpP7Z/WtZWT8QixN1Mtx5vxgBlX
AB461HOriNG/QnR0nbaZynrlckUxmW10uGK2S5UTzpuErdz25/asyA9sWcOZ
IlOPNI4+voahlvbe9E0VwFmi3KqSrz5QPX8+a4vFzT+Rwmv/AAdfhtc6yly3
1mSa9NnegPLzHuJ3itfpVlGI1iOFbb8QRR+HXivOLCwuZ4pbvzBw+wzSOFAx
x1NbnwvrOnTnyBeIZohsBYbBJ7jPWvTtJbPR/kY44QTg9/Q61W5stN0qQXWU
gcGPCruJLA/9+a810/TJ7u5FtJbNLCzbd/Kqvo3rWm8S6xDdynT51P2cuCjI
fvY45/Glya6ukXUXm7JIYv5bNHyQh7ZHccHFGOcHLg+w/jcsccJQ/wCb+y7N
4SjFmYY7whs8jy+ODxjng0raOSTxHb6HJCV0+VQJfg+KQKM8t6ZxWwinWR1Y
SLJDMA0Uq9Dn+9c3Vosw3D4ZB0YdR8q7XhjI6MrnmhwkzPa34DsrmNZNOb7J
Mp+IElkZflng1kn8F3q69Bp5eNlmUv8AaFBwFHXI9eRx7ivV45fMiDNjdjDY
9ahRR5obA3KCAfrTPxsct0csvCxSd1TPOr//AMOHtru1axne5RpVWVHUAqp6
tn0FSXuiXekeDdRhvvLbADxmMkgHcPb1r0VcctVW5tY9RzbTxLLbhgZVYZDE
chf3NLPxYPoWfhQpqOjyLw54fu9YMs6FYrVT5Tzv0BPp64/emH/k7UyjSwiO
RMOVBcKxAOBkH1616usVtDD5YijWIcbFQBfkAKzWr6DA8UrWsW2/nYgNH1VD
wR1wBiufJ4/FWck/D4pGQ0DQbq+t7u4M32aOJ1VhKjckc9uncH507tJtL1Gz
k0/StPgNw33zIQoYY5YZ5YZ47VHptjq9np0kc6faIzKqCGSUnacjH0B5OK7+
x2+l2989rO0GrRjMfl8qWIyqjdkHIzkVDhSJ8IxTTFZjkksp49eWZZc7LWQq
SVYHO044wfeoZY/8C8QABxj2HvVOfX9Rv5IINTuA3kSH4WjCEN0ycdT1poLK
W7RzDtYIuWBYDI9Oa4M6lLIlFEG+b0hJHp8Mlr5ousXhkCRwbOCvTdu/Ouls
EgeQ3GSyHaiQ/HkDuSOPrVm6eOEMsIQzbQpdzxj29KoB7sfF52VUfHsHTnHp
z1FHJzivQtK6SNJpltNe6WTGhWDc2AHwSe5JPNfNMuIrTW5LSWEXEQj2eQRu
ZD169D/Y01gvo7fSf8LA+ETc0sq4APqf8x/Kl2i6bJaXrXbEtdRL57Bj94lv
iH54r1oeMoxS/wDZ7ePx6jFP/s0M1vp8b201vAYJHI3KmUGD24PyqVB5dwGS
3ildeMSSsJPmCTj6Zqtq+qRXEEb2YZVE2xm+6UbHwg/PI/EU20a+t9V05HdQ
JSSkiMOdynnj1quLx4Y5Pj0XhhjjVpdjK3vFKIs4eN2+5v8A0+dXtjLglSM9
KR3TXlmN0UMc0HRkY9R9ehqxZ6/Z3DxwfzUJGPiXOwj17ikzY1F2ed5Xipfl
ETw21lNdyst15aFziPI+HnpTIadalBi7P5VSt2mLuX06OTLHLKQQ3PUVfWS2
uIZolskeWNSZoQBvUZ7Dv9K8/i2zl3Q68I20dteT4lErsMFvQdQKKPDO2OfK
25gUscKRgniirQ6OeXZfY5Y/OvlFFOYFZ3XJJRclBZCX4QVfGePw9c1oaQ38
4E8jM5ADkDHtU8nQ8OxG0c15ayJJFLZiNS4KfCWPT0rz4SzWNzLMsJkhclTG
54f5Y+Wc9q9RNxEwPTB7N3rH+ItklykpaNM4SOFEGyNAeeffvSRpqmel4+aH
B48iszyWX8Zg321rIhJ3CN2JU+6kcZ+dSWIl01wJ4JYecfGpxWkit7OwuWe3
EcaPGGPlggnPOMDgY9R7VPNNBLZFLpRIrLzuIIXHqaHgU4W3Q0fBWXH8rkIr
7WbaVlgcFGkGWfoAexHofen2h+KVgSHTLi0jkjxhZIlAIGOpHQ/Okdrpel3u
oRJJazENlg0LFgQOwxk/Stfaz+GrOHybeWGNV+EgRsDn0ORmujwY8VSkHh0k
4t3/APS9b3On3kPl20kYHP8AL27CD/y/2r79peE+XOfZZOx9j6Glk0/h6brd
pn1wRj8q5+0wbSkGqQzof/TmYE/j1/WvTX9nqQj9jTzSHYoDgj4h6GoGvwry
BZI92DtwwPOeaWzTs04toSwVVBOeck9vcelT/wAEkWM/cywzszz09a83P/IR
xyonPJCPboam4OyOMMFduM+nrXaTwrGUjcEIcHB5zWeSU2s+04CNxzxhugx7
0fxBYoGkCkDvI/wIPqeTXZgzLLHkVjBSVjvzTLKoJC88e3+9LdUuJZbqTTLY
yJKIw5McgEijPLcdPQDvmsze+Ir1966apAHLTOMbvYDsKg0K1uX1BtTErWk2
QkqoMK3f8/SsyTt8UcPkSd8Yj2/khtLa+uo7gGFFVpo1fDM2eDuHIPbpz9Kj
s9Ys9S0+ENa/yVnCMHCMGYj75475xx3qD7THd29y1hCIY9388MBho8Egc5wC
fl3qC5N3baR/Fba2jt7tCN5GNu1uMbenoa5XfLRxzjxf9IkfQtOluriW4khu
C80eWQZ8o4OUIHQEAcmoNXsV05/M2NFA5xGCckH5ZqOw1jUNSuDeTyx4hiDT
KiYDx89scnI70/tJv41bwM9vI0qS7UTG4lseo46H8qnPHHLFrr6JcOUbRmvs
9rdQgeaWuiSSuPhI9u9aTRvCSvYIL7EkQZZFRGwp6Ee/51WTSbY6jOl7cIkz
uTEYsoEI4K7T3Bx165rcaPYNDZEMWYM3KsOMEDkenTp71GOBpq6pCwxq7ZVf
SNNuykBjJTPmqquQFIPGfXB9azmq2Y0S8S71EtFayboxKhyFOMgMR0zTOXUY
rPxRDaqx8iRWJPv8/T+9P5ZrKaCSzmCPDICrxkZDAjpXVHI4nouOTErj7R5h
pmoWuo2et2sKs4MgkQkHlSMZ/Fac6ZdmzVXkt2aCZ1e6mXBETAbSxHXnCn8a
W654ck8E63He6erHRbzZDNvbcYyTkg98cZB+dSyXcLM9gtz5YEgNwQMgxA5I
PzHHtVvkuDd7Mh5LlifLtGutNSs763M0F1G8YIBDccdzSO4YWV7IcKDICY3x
nIxmqwmstP0Wa8sPLexlYRSM7fzFycFlI4OODgio/EGo2F/YwG3uFMwZVjkZ
Qu5SSBjvj1zUPmluxMPlOE3e0RWuuTfaAsdwTCx2BMY6HPA9Ke+H5LdtVkKt
M87p5kkshxkNztGPxwemazMfhO9JucXUEi2qhm2MecckD5Yp5b6ncTWifZYW
iD4/xEhALpjAxx1965vy/aTObK9cr7NppD7Lp1afe4Ysf9KkcUUj0ENcyPu8
ryVAOWViCTnvkZNFPC6OJ9msor6ww7D3NfKoKfKy+qRhbyeMu6hn3cN2PNai
kfiC3gkaFpHaJyCNyjqPSpZP1saPZm3gRTtLNtz+NKNT+zhHEagt1AJ4Bp29
rbIMteSbSP8AL+vtWf1bTrG7YCC7mMgIAyBtOTXK5WikutDe18J77RLx9ctE
hIBZ44idnHK53Y/EVl7klnIDFkyQMjGQfarUVjJaPJG1wZmzt3jeMjpjDAGu
Whw3I69M1yZszrhHSQnzT48L0T6Glgl0v2/4baJSVXnlu3TmtaL3R7+4Cx3U
csznhdrbj+VYpUyvPyrQeGdUtrJJ4ZjFD/X5zDBI9Ca7P43yWprE6p+/Z1eF
m4y4j86ZB/8ACX8Kim061Cf8CNvmuaXSeNNOuLxbPTvOvZ2PAhTC+5LN2+QN
NVSRbcz3m3d/8JCSMnoMnkmvo4uMuj2seXl7ErD7PeMyglWwwKD7p+XcVfbX
Lc8vARKqkBicVNHASgY43Hk46Z9qq3VtGGVizJ8eMqccEVw5v4vHkfIaeCGR
7KDHzrhZnDRpw4c+5ruPQbeZc3QLy9dxYkfgelNIbRYyVOSWHU9/+hivisLJ
xHMcQMcI5/o9j7fpXTh8eGGNDpJKkLZtDhZdpQEdvSvl/Dqa39stnDGsG0Ft
oB6HnOecAHjHc1oJItsZbggDPPcVmvE2rWltpN7BJPLBM0RRQpKuSemCOtZl
ikuzjzvXL6F1zd6dqdtb2SedCty25zCmzeM42tjjkisNeWF1ZXTwXMbxpGu4
7j94E/COOP7U10zxci28EF3p0UjwQCGGeI7XU9j/ANquWkd9rl3KSZL0MrJ2
Ynb2we3b61xS2eXJxyK/Yu0S9gg1K2u2EkkrPh0U4Bzxj3xkGvRZrhw4isol
RUViV+4S2OAPr1rKWfhjVYkW71S2gtbZCBGmFMmM9OOF496cSwK2pG8tGkll
QbVRvurkcknPI7/nWQk9pofBFvTHelNcasjT6nZ75xKCMgjJAGCAeeP2rSW9
3kLGCWVmOcsOD9KxtjPfy6usFxB/h2QjevxBT3wR19K01tGE+KMxuCOo7H9q
HIuoxlZD4ut9It9GaY25jvIQWimiTGG75Pcf96znhy+tJHE096jPECArPl3k
PVj2x2Fa271BCn2aZPMd0J8vGeO5Py9axmo6Zd6Zdj+HwWwhcFyjjJiUYyc5
5HIP1pX3ZfDm44niff2Pdfubq58N6hZ2MMN0WhZZftDbQARyU9T6ZIry/RU+
xyNFcy7ILjCXGF3sF6g/Q+lPr7UJpoRE8n8sc46An1P/AFxWduJAjkKOh7VK
c9ni5c1zuJtYxpdlpckVs0NyhlCCRnyGQ9SynnuR88Gq95avbW0EH2KFIEcS
KJXzvVQW2ljjsMZ+VItKKXWrRRSKfjYFeT1HPIHXjNPfE2k+XH9uiZkh24ET
FiS2eDzkAVnJyi2VUnKHM50VtJEM2pXEm6N3/m2BDblyeDnoRyK0rs0OmNK8
PkxDLRxhgw2fP2GKxOkXyW4dbuG2ubfkTEkjjGfvYOOR0rXrdreG2eO03BYm
MiEkIR6gt09xRCblF32M5OcbZSt7jXJjGLKznjglbzIm2gq+AQf2opxo2nTy
apY5nmZUD7vMfI5U4HHGBg4NFPBVFCckjZzjbcSD0Y1HVi+XbeSe5B/Kk+oN
qgAGnpbMD1MjYI/amk+KsgW7i4itYTLM4SMHBY9KyesXmlXV35hvp5WYcKG2
qnyBAqLVby6imjg1F3W7kQ+VFbv94c/E2BgAevWsyZo7dwTbeZIGwPh5yOvD
DP5V5+fO5fjX+BW6Lv8Ag4P/AOHPdXIOd9xJvT6BeB9RVKK4SDUPMLRxSK25
I3/obORg8kUuf+K6nM7RRzLGGztAxj8gM1Fd2ps1BmiKSP0AYZ+ZxU4xlW9G
t+x/qWoi+1CW4ZURpME7O5wATVYMvRxketJ4maFkiZWTcgcBs5Oee/0NX4p9
vDDI/OubIpKT5GE8yhcnI55AHpS2/j8yzuEyclCePxppIA6JJkEc5PTpzSm5
mKwSMRyVYn8KzE6kmNHRpP8Aw50ZLfRjqkijzrpiEJ7Ip4/ME/QVqx/ipVcZ
8lD/AC/9R/zf2rzDQ/Gz28Flpd4iR6fDH5ZZASWPYt7Z5IFaS68aRXut2mla
PJlN2+W4xw20Z2r7ccn8K+sx5YqKSPX8fPjUEkzYRIoUpxlWxil0irdTSbcG
NSVz2J7/AKVAlzdandzwfy4UADA7dzf6iD27VcFxp9tvthcRIbdAXUtyM859
6sp32dsclO2KPtt3BNHA20xxyKFdvvZx0PqOafiKG/tWUjKOCpB6j2NZjXQZ
fDGqXQyjlDKnquCCv14FLvD/AIut9UtPKnna21MLtYIcCf3Hv6j8KV5adCzz
xUuPTHEKXkNvsimd4cf8Mnr8s0g8S3lhq+mSbwEMcZKyOMMrDoPx/Wm99r9p
pMDFpY3SNF4RgWz6Y715hqepnUr0O6hELFgqDpk9KhkypKjl8ryIJNfZb0XS
o7lXmllMSEbYnxwZew+Q616X4A0iHSb3ULYXMUsjxpIGVeUHIweccnPvxXn+
jatbQbbXUcmyTmNkHxRv746g+lXrHx21p4ltb57RY4owYp0tzjzVPUnjtwcf
nXKmqPOUoKKrs9B8QWn2hGS4mPl8H4BkAjnOO9In0drnymtLuSOK2X4lXIbj
H4nAx6c0z1TxNpmswRrpdyk7SDdz8JTp94HoeelVobOSKIhJm3nqO1a5L6O2
E01Y2F1Iu23t449pIJcFgfoKeWaeYqBYgCi7RkEDnpyKx9uih4ZbiAOpGGlm
cBU7kgVfh1qxk2KLqVE+7lAwwo6ZBpW77LNJJJFzxRYQzaa7mSOHUIEMkZBy
SAPiVSOcEV5zHqt4qSJ9pdklTYdxz8Ptnp0rd3N3ZQ2dw6yLLvU7mPxALjkn
6dq81Z1DkoCqE5UHqB2FcXlNppxPO8l/lpn28md9kYJCjBZ2P5VWjlRW+CMu
/LNI4yM+w7VDfTfzVDZwV6+lcxLtt1IKCSQ9Ce1avzinI4utIu2ZYX0CxAea
ZAqjoQSa9SZP4jpzw6osRQw7D8OAvoTg49+PSvLLRfK1C2aaXYFcEyEZAAre
XmorLpJheZIw/KoVwXwQeT+lWx8Urs6vHVY22ZnRbi1gvjayws8ckiodv9RB
4AHTnA61tbVlEDLLM8QJAOzGY8f0n3rMEWPlbnmYuBhQoywHcbvT589auR+J
ZoH+C3iZMYCycn8RU/lhFU2ZySR6b4ehiWWAxxhR5ec7cE8dTRS3wHq95q1z
MZrNY4YouJVJwWyOOfaiumElKNokaTU1xcq3qtLp5lgiaRlZsdFQZZj6AdzT
fVU/lxv6Ej8aV/rTgYLWfFk8N5NHa2X2GdgFeaVf5xHb5fnWaeZ7mVppZWkl
c5Z2OST6k16drOg2etIn2jekicLImM49DnqKx2q+EodLKSLJcTo3BwhXn5jN
QlyW2TlCTYgGCcMK4kiik+GRQc9Dmp3l0/LIJmjK9RJ1B+dV5giopc4RzhW7
GpPIn6M+KS6LOoyxXmlWdusIS5tmYCVcAuCeAfcVTt2kI2yxsko+8jKR9R7V
3ItxaTiOYMkg5VvXH70xlvpddvIIfMggdRmVSNqv1w2T3z8gM0mTGssf7Gjb
dPsjiXzIJUHOVzjvkc0p1CGWS3nWFGdhG2FUZPA5P608+y3Gn3ixzwtHIpDA
P0I9fl8qz3i0tY7beNyrM+8Mp529QfzFcOPG/lUWM9GSYEHkEGrOn3UljfQ3
UYzJC+4D/MO4/DNTR3ySaa1nLYQyShi63C5EvuDzgj6VTRgHG1ufQj9q9i2u
jE2naNVfeNJP5bWCsshJ3mQdB0wMGs8uqXg16PUHDSzq4YoQeR3GKt21gsjx
O24RSZxsOCvqcn0OKtzlkyFMk0h5duWZj6k0S8p+tsu82Se2zQa/4jWfRDbW
0ZMU6qJDIuNqnnGPXjmsLNdBASu1c+gxXUt15oIJxjtnrVM4d8vwvqaxyeSX
KYmXLLJK2dwqsxLyMcc8dM4GaIo2YghGIGM4XNfVUShmVSzL/SP6RWg0PTY5
La5lmaOQFGR4GGHHGcofX1p+L7CMHJoTJaSEEojMBg4wc13c20cKKDIpkC42
5HzOT7+lPJ9IhtVVorRvLkRv6ySg28/maWali1dIk+48YYOx+IH9/rUZRmp9
mTg49kvhTXLXR9azqEX8mRfL3Af8I5+9jvWz1rxPoh0txZXn2m6mUpFHHlSr
EEBjnGOa81ad3XLS5Xp90cflUBXy3DByCCCOelWTQ0MriqR+idA0vzNIgjuc
O6LhmbnJAxzWT1i4svC2qzWkkZuE2hoxCQxyc5Df5ewrJwf+JWu2caJGLcuM
5eRC27I9Cce9K9W1ufXr37bcxQR3J4drdNm/0JHqOmayT0Wn5DfRb1LWZ9Su
cBTBDnCxKevPf1qJpMnPA9qjtbq53f8AEZtgOcnt9amS4cNuZInB9Y1/tXBk
dvZzSdu2U7mHzimWHBy3yr6paSVG2MEUgBiKYRSrvBNuhOc8ZH6HpUheO5lU
CBUKjBVDu5z+9Cm+LVCcf7OYm+NQmGYcg9hVtBlgDyxHQVcttAu5pCkNtPnY
HKlArY+pxjPpUt3ptzpsSG5heCN225KcE/PnNS+KTX9Gq+ikV29evtTC102W
SMSMqqp7McE+9W7HS4ZSJFmBPbctPYtMKqC90uM8gDqKtjw07ZVR9s2Xge2a
30QbsZZiRgdBnNFONHjEemx7RgNyB7dqK9FKkITXyeZaSDuBuH0pHWkYBlIP
QjFZyRDHIyHqpxWgc0p120FxaF5mkeBCC0KMQCPU464ptVLVA38PkKEBgQQD
356Uk1aZq7MVJbaTGCsUTxn1Tg0j8RWsUenwvG855yBI5OAfyxWpmkuTnFvG
aT6uWn06aGWFvNbaEbdkDnpXLqqKyX4sztrq0m4/aIlnyuwlvTtx/bFXLI77
pFtQYy+Fd5E3BR3wBz74qWw0FJI0d51jjZiqsQTlh1Geg7U30WO4sbK5luJo
IrHfguluzPC6kYYvx36YJ9xzQoS7ILk9sd2um2UNr5K3GrXoJ3Yki3KD6qGX
C/Q1mPEOivqUjxvZzbkXEWFBcAeuCcda1djJq+rWD3lzOEgdSIIIcRmQZxvZ
zkqD1wO1KryCTSLcS6fd3InlI3ToqLEcclVQj7o5PAxz1yapONrkx1vR5Fd2
0lrKySqykNgZBGaplS5H3j645r0u8kbUTLPcQLKWx5hePCk89MDH71kboTJk
RxwwEEg+Uo6jtu/7UmPLOtxM+OiOyaaR7XTLeV1MjYLOvKg8nH0Ga9WsPD2n
jSFto0Mibif5oG5s9z715La6deOgvreQRNGSBLJKEywHIUnqcHpWrtf/ABBZ
LTZeWOZcYDRtgH35HFU4rsrCUV2UPFPhqLTbhpLZtqsf+GecfKs0UMgCiEyA
cEpzg1e1TW7rVrvMkoDHgAdAPmaBdJBbCK3iDt/UQOT+1TdxZzzavQt2NGG8
oshb7y+tW7HVbywE32Jgkk8ex3I5HuD2NQu87sS0RI9MV3FDuIyg3enpT/LJ
IFJokttV1SCe5k8wO9wmyR5RuJFUirvITO3J+EZ6+1MWijVFVwIyRlTnr71S
Y+VKdwUopwxLZ59K3nKTprYXJ6ZL9jJtuFUYbOQRkVE9ihKtv5P3gMcVZiuV
OCxCqOuB39atwWcN9cpbwyiOaQ4QN0Y/tUuUovYJehRJEpGZHGQMA19iQwuB
klWHBIxVuLS5biZ4kJd0PxqFO5ee4q7qmiSWE8fkXMdxuQscIfhA755AFXSc
o6GUW1YW0R+yvJ/mYLn2HP7ipAoHU5qe2sriSyijgMUkmPM27wMbu3z4/Olk
iak888DwyxNAMyjbtCD1J/6zXBFc5NJmPQ0tbdrqfyY3jRwuTvfbxWg0yyj0
+VjqFsz2xPxyQurEe5AO4Dvkc1nvDniGPRwyGKaQMfvxygMPoykGvR7fV9Mu
o4kkuYbuWRQyoyIWAI6HsK6ceKC7ZiJVmcrCftCSnP8AhL3Pwvn/ANOTHQnp
79eCMUt8V3C3dnYlEJfe++BhypGAQfcHirOtaMEsJbrSlEE6ruaOPhJlHUFe
hOOlJtSMmoeHdPu7hFS9uJCQ6ZXKKMA49SMfgKq206Y8eyWyVNoZrKQcZyoN
NoPKlmRI7SfccBQzNtz+lKNPj1FI1VLkEYx8XP7VpvDtvqNzrVtFcXAMSNvf
GPiA59PlSJbKt6PSII/KgSP/ACqBRUlFdREKS6nHsut4HDjP1p1VLUovMtSw
HKHP070AJagvI0ltJVkzt2k8HBGOQanqC8OLKc/6DWPoEY6W2umORcvgjnk/
3pPqNtJIPjlJx0Nad24xik2pA4OK42dA20+GG48F20iARvAjDK8fFnBPuT1r
Py2DapqZU+YLcSrvt8MnxYzgA9SR6+oxWk8FSb9InibB8q5bGfcA06isIIZT
KFLSFiwZuSMiuqrRNSq0SRRRwwRxRLtjRQqr6ADAFU9S02C/Xe4bzkX+W4OS
PoeD9avmuTW0Iecz2V7GjPPJNELeR4hHuOwttyp29OCfr9aQXunxPbESK0kp
X4pAQNxz1P0zXqmq2yyaVeoigNIhc47sBnP5V5VqNxJFC7/dBGNyjJB+VRya
ZRO0xa99bwaPLYl4VkEoKqsALP65fsB6VTltEuC7wzyGJRks8O0j54J/KtN4
O0KTV5biSad1tlwsyj70meg9unWtBr+l2/nfZrUR27BEKAfCOmOv0rYxbViQ
jzls8oOJTiJmKgYyRivqvPGgUYCjuBmrV6GN5KY8CINgADGT3P618CfAA64b
1xxj1odPQlJumVQJ7hgkcjEk8AGmKWcsFzCxL7wuVx146n86saaFs70z7Qy7
CoOO56Grdje+bqrSsOCoCA9sHn8f2pJqT/X0JNNPRNHFeyLtiVmyON3ApJq2
nTrqUEGNzygAdBl+/wAu1OF1O6ikliWSTy1dvhViMAnoDU7xLeW6tFIEETCQ
OPvg+5PSkx1CW0EWk9kQ8OQyWUYjaS3vGUb1m5B7cYyOvSqFtpUkG6WSWPfa
zmJowcknI/DrWq0+/i+zQJMRLcBiXaXOW+R+ePwqjDfQ3Ud0r2JgltFJ2E5J
AJz27YrpmouLqmdcowq0c2mpONZiVZfKebb9p3oPTlgT6/kc+tT67qf2Tw+s
ljcQMsrPHJHIgJyeMrg+n45zSDxDbix1dTF5p3wrIPNbcTn27ZHaq6RRypse
MhW5AxnY3sfT1FJz+HXdE/lcbiUdNa+ieSSxLho0LvsGfhHUkelWbvWL7Uni
e8kEoT4em3cOuDjr7Vs/CFnpltMLvdKt6FaFrcfEJM+gxnt64rTWvhrS0eaZ
tOhVrgfzYc7kHORgdj7ilUFN8l2R2eX280EsCJcRrHGhKnyYVBJ9SSe/5ela
7w5bWWoI+mrDHBGfjVlkLS7h3zjBHtWquPD+lXSyh7KIGV1kZkG0lh0PH/Rp
VN4OEF0LnS7kxENuEch4XnPBH70Sxv3sKaHGlaG2mkhdRuJIjn+UwAXJGKVe
JAkV7ZWsahY4YPhUdhnH7VpbI3RgUXiRiUcExtkN7+1ZXxG+7xKI/SBP1NO0
ow0isOyxZECNWFbjwdbFnubxgccRrn8T+1YC2mCRkBc4FeuaLZmx0m3hYYfb
uf8A5jyaXEt2PN6L9FFFdBIK+MAykEZBGDX2igDNzxmCZ4z/AEn8qp33Nhcf
/TP6U81aDhZ1HT4W/aktyN1rMPVGH5Vj6Ax7TygkEjr1xSzUpJJUIDbcHt3p
kxR03A9BxSm7c5I61xnQx74EY/ZNQUnkTqf/ANa1ZrI+BnDDU1A6SRn8jWtN
dUf1RCXYGuDXRrk0xhHJgqQ3Q8dOua8mubIzXJEn/CjJAT1Pqa9aJrz3VYhD
qdynTErEfr+9RzLSHgr0X/BCbLfUOn/FQf8A6muPFyhbu1kbgFCMnjv/AL11
4NkENvfq27HmqchScfD3xUHieBJWgkja3lJY52oc9uvJrJSqBj1Iwd9Gv24k
bWQnjHAA9a+yRqMrERtQKPqRz8qaXVi0w4RVjzy+No/Oq2VnllRcKjEbWXpn
j8uK5YZFOdIndMoqkqqCDjAyRVmNwx6A/Lgip/IchgchxwwHao57bylVsHqB
x2J9K9SMVVlLPvwZLEda5MgR8pwehI7j3qRI5jGCpWRD0PFVZWnhPNpI/uoP
9qGk9NGNJ9l62kaKRpVwdq55HpXemRx3PiCO9kjLN5obykBOSOfrXOjxXL+b
PPFsjYbNjDtTvQbNbfWU2/dwzKT8ulTnF1S6N1xoy8ltNqL3WqsjlGmw0jnA
3HsPkKkt9qPsUbhwSScAD962PiuNItEit4UVFMmFVRgdDWRtF8xkgJxskG7v
wO+K4c8HH2Rao3Vnpa21rBcQxpmWNRcI/Ru4Jx0x69vxp1aTI6eXlxIo5SRs
sB657j3r7bjEEY+EfCPu5x07Z5r79liLq23G05AHGD6j0+nWuuEOKVDk9drX
ArtaqaSrWI8QZ/8ANTEDIEcYP4VtxWN1kbvEtwfRE/8A61PJ+o0Oxt4UsF1T
W0UofKgIkfI9Og+pxXq1Zzwdpn2HSBcOuJrrEhz1C/0j9/rWjrcapBJ2wooo
pxQooooA4ljWWNkYfCwwazU0TRStE/UHHzrUUs1a0DoLlF+NBhvdaAPMkj2I
2TjBwaW3K/zCexFMr1GjvblMkKsjcfWl1yTkY6dK4/Z0ehr4IAEmqY7vH+jV
rc1kvBP3tTPrJH+hrWGumH6ojLsDXJozXw+1MKcmsR4hj/8AbMuOM4P5CtnL
NHCpaR1RR3Y1jtcniub7zYW3LgDOMc496ll6Hh2TeD+P4ivfzEP5GrPiZBJb
2xc/CshyT6YrO6bqF3ZXNzHbbcyhSSVz0z/epLyW+uXU3bkpnAyRx+FJOV4+
K9mtO7KVw/nbnGREhAz7ZqiFjVxJGMZkIX6k/wB6cTxobcxp93HHvVL7IGsn
QfNT6HAIquPx4xSSF4ld7ZmIKSOrx8gp3HpVuwmaWUDU7aGSBPvKhKmQY4wR
kDnFcRRGWMkDD4wyg8g1KLeYH7owe9dFejHFNFtxZ4IgjCkgt5fHxHvg1Xt7
mKSQyWwBUfCyg4ZPYr1FfXsrl1GAQwIZSO3/AF+9cX2kK0ivIpSXaNskbbWx
7GnTYUi87JcR5CEZ79aqvP8AZCnxgMrAg+lS2kL20BEshuD2LyFSPnjrVd18
ssNwCuMFQvbOetLk5PSRkaRa8S3Qns7Bh/UWY/MACqtpo6ahcIgkaEkDc6AZ
OOe9VL2VpZLRCxKjP0JNOtPnEEwZeOvNcWVJzplONxo1MKOiAPJvIGM7QKmF
J11GTswPzArsalIDg7f/AMarzQnFjeu1NKRqUn+RSK7XVcY3Rcexo5oOLG4p
HaaS+seO5ISD5CKkkx/0hRx9elXY9Vt2OG3p8xkflW10XSo9PSWcr/iLkq0p
PbAwF+n6k1rqQLQ0UBQAAAB0Ar7RRTGBRRRQAUUUUAFfCARgjIr7RQB5p4r0
99O1SZyv+GuRujbHQ4AIrMzcgH2r2PVtMh1bT5LSbgMMqw6o3YivINRsrjTr
p7W5XbJGcH0I7EexrmyRp2Wi7VFrwvf2thHfmdyGaRCFAyTwaZv4k3HENtj3
kb9hWLimEc0o75BxTKGQMoOcZplNqJjjbHUmrXcuP5wQZ5CDFQSXUkn35HY+
7GqYJ4ye3UUF8Zx1+dJybNpEhlxnIAqhcu7PuOCAQanL5PIqCXLE8dqxmooJ
GVujPvxxjaOlSSuzjkmu3iODgmoirUICNbgvI4Q5SNeT23H/AG/WrkW14Qw6
kheD3B4/Hp+FVlwgZSNoAOe1RW8/lYEn3JEByfcf9f8AQrsjL2Y0N7eCMsrA
hXJ4Yn73saZyzWtvbq8pQL/qGAf/ALvu/nWdkla3ySWKc4frj/m/v+lfftpw
WSTCN0KnIJ9Kqpk3D6HDaraENsKY9mzVAanBPExVQxQ9TyM+g+g/OqkrB9hk
hhfAwWZRx+XNQyS5JAAXHAG3H4Ctc7Rih9nFzqSxqvmBhG3SVQduPfuKrpeW
78/akYexz+lRyyPb3LKOVYB2UDpk4yPwqzA1rnzEeEe4wCP3pHNspxomh+PB
KYU/d3dcVfjGzjH4VHEhY5weatxpx93pXFJ2zTuN8EZNWVbPaoo0JHYVIqsM
c/P2rAOwd3ORQQf+1GzByG4NMtI0d9VvRbqWCdZHH9I/v6VvZljLwhopvLv7
fcR/yIW/lg/1P/YfrXoFRW1vFaW8cEKhY412qKlroiqRNuwooorTAooooAKK
KKACiiigApB4o8OprlkDGQl5ECYn9f8ASfY/lT+isaTVM1Oj89pbvFq09vcx
NHLGdrIw5BpvHHkAFRx3r03xF4ai1YC5hCpeIuMnpIPQ/wB68+lt3gkaKWMp
Ipwyngg1zSg4lVKyq+UOAnOK+/F0IqRsgnAJ6d6+kYyTgcVhpEyAd6iYZxgG
rBOcgkdM1Eyg457+ta0ZZV+IgcYqEbiMZHftVpo88deTUDIoY5Hf0oApXMKu
w3sxXoQGxmorpgPK2/f5wPYDNXbiMGM5Ax3pf/h7aQlsq/TJyTj2qsHao1Ek
Vw3l/wApQwGMoDyvyPce3/aomigmcmN/Kl7hCUf6r3/CqeWQr95d2SvZhz7+
vHBqwsomVvNjWdV4yByvzB5H0qqYNFgQXATH2vI7b4lbH4YqHyrzJBuoV7F/
KJP5muQLUHAknjB5xuf9DXErwnrfSnv9/wD2reQUBgSJHDO0k0uF3OeW+Q9B
1qzZBpAvwIzDjeQM1VhCbt0cbAkYMr5yfx5NN9NtDs3kYycj2FTm/YN6LkEJ
xzVoIg5JFfUh24461OIhkY+VRFIxhQQqliOldKJSc7V2nsetShQVyM8VZsLW
TULpba3G+RvwUep9BRQH2ysp7+dLeCIF29+AO5PtXo+l6ZDpdmsEXLdXcjlz
61Ho+kQ6TbbE+KV+ZJMdT/amVWjGibdhRRRTmBRRRQAUUUUAFFFFABRRRQAU
UUUAFKNa0GDVo92fLuVGFkA/I+opvRWNWB5Rf6dcWFw0NzGVcDIOeGHqD3qq
0Y5Bz3r1e+sLbUbdoLmMOp6HoVPqD2NYfVvCl1YbpbcNcQDuBl1HuO/zFSlC
iikZsRKuOV6cn1oKAfeOK+tBKAojVhj/AEgYrowylQDG3TrnFTNISqkt944P
p61C4QykAMSwHQVbMErMT5Z577sV8ktZDgso445NAFWSJSjfB9Mil1zC4IKA
AkA53dac+VtPKp/+WKPswZQPg49/WmX9AZOZWlxHLsUBgTzyapF9kjRsQxHC
knBx7N+xrXXOkw3OQ+3PsKqDw5GxGLiUgdB5ef2qin9jKSESzunWadAOgZM/
n/vUivJKuRcMSfSP9zT1fDmDnzZQPZMfvViLQ7dCGkSZ/TeRj8M0PIFoU2Vi
85yd2OzNyTT6G3ZQMZwOlTKscIGyIEccbufwoW5VwDGkfX+pjU3K+xT6IOCu
CRnIyehqRVOOenuelEbyO4VUjUngYUtn6VqdI8K3M5WbUSIo+vlKuHb5nt+v
yrUr6MboUaXpFzqk+2IYQffkP3V/39q3ul6TbaVb+VbryfvuerH/AK7Vbhgj
t4liiRUReiqOKkqsYpCN2FFFFMYFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAF
FFFABRRRQAp1Lw/Z6iC2DDMf/UjwD9R0NZPUvDWp23Mam4jH9UR+L6j+2a9C
opXFM1Ojx+SFhIQ24FeCDkH8K62oDx9a9UutPs70YubaOX3ZeR9etJLnwZp0
uTBJNAfQHcPz5/Ok+NjcjC5RR8KDHyroSqCMKBk+laSfwPcAHybyJh/rUr+m
aUv4fuLJSGaEhfiOGY9fmKWmjbKfmlRXJuGBOQMdjVl7KVkwDGPnmrUHh27u
Nu1rfJxnLsBz9KygFTyP97IJx24qNVDbXb4Succ/jWvh8DyEAzXiL67EJ/U0
ztfB2lwAeYJZ/Z2wPwFbwkw5IwCxvcSiKFHeXH3EXLVoNN8HXtx8d0RbITkg
8v8Ah2+tbm3tLe0TZbwRxL6IoFT06x/YrkLtO0Wx0wAwRDzMYMjcsfr2+lMa
KKpVChRRRQAUUUUAFFFFABRRRQAUUUUAf//Z

View file

@ -57,4 +57,9 @@ ActiveRecord::Schema.define do
create_table :lock_without_defaults_cust, :force => true do |t| create_table :lock_without_defaults_cust, :force => true do |t|
t.column :custom_lock_version, :integer t.column :custom_lock_version, :integer
end end
create_table :audit_logs, :force => true do |t|
t.column :message, :string, :null=>false
t.column :developer_id, :integer, :null=>false
end
end end

View file

@ -31,8 +31,18 @@ class Developer < ActiveRecord::Base
has_and_belongs_to_many :special_projects, :join_table => 'developers_projects', :association_foreign_key => 'project_id' has_and_belongs_to_many :special_projects, :join_table => 'developers_projects', :association_foreign_key => 'project_id'
has_many :audit_logs
validates_inclusion_of :salary, :in => 50000..200000 validates_inclusion_of :salary, :in => 50000..200000
validates_length_of :name, :within => 3..20 validates_length_of :name, :within => 3..20
before_create do |developer|
developer.audit_logs.build :message => "Computer created"
end
end
class AuditLog < ActiveRecord::Base
belongs_to :developer
end end
DeveloperSalary = Struct.new(:amount) DeveloperSalary = Struct.new(:amount)

View file

@ -12,13 +12,15 @@ class FixturesTest < Test::Unit::TestCase
self.use_instantiated_fixtures = true self.use_instantiated_fixtures = true
self.use_transactional_fixtures = false self.use_transactional_fixtures = false
fixtures :topics, :developers, :accounts, :tasks, :categories, :funny_jokes fixtures :topics, :developers, :accounts, :tasks, :categories, :funny_jokes, :binaries
FIXTURES = %w( accounts companies customers FIXTURES = %w( accounts binaries companies customers
developers developers_projects entrants developers developers_projects entrants
movies projects subscribers topics tasks ) movies projects subscribers topics tasks )
MATCH_ATTRIBUTE_NAME = /[a-zA-Z][-_\w]*/ MATCH_ATTRIBUTE_NAME = /[a-zA-Z][-_\w]*/
BINARY_FIXTURE_PATH = File.dirname(__FILE__) + '/fixtures/flowers.jpg'
def test_clean_fixtures def test_clean_fixtures
FIXTURES.each do |name| FIXTURES.each do |name|
fixtures = nil fixtures = nil
@ -100,7 +102,6 @@ class FixturesTest < Test::Unit::TestCase
assert first assert first
end end
def test_bad_format def test_bad_format
path = File.join(File.dirname(__FILE__), 'fixtures', 'bad_fixtures') path = File.join(File.dirname(__FILE__), 'fixtures', 'bad_fixtures')
Dir.entries(path).each do |file| Dir.entries(path).each do |file|
@ -174,7 +175,6 @@ class FixturesTest < Test::Unit::TestCase
end end
end end
def test_yml_file_in_subdirectory def test_yml_file_in_subdirectory
assert_equal(categories(:sub_special_1).name, "A special category in a subdir file") assert_equal(categories(:sub_special_1).name, "A special category in a subdir file")
assert_equal(categories(:sub_special_1).class, SpecialCategory) assert_equal(categories(:sub_special_1).class, SpecialCategory)
@ -185,7 +185,11 @@ class FixturesTest < Test::Unit::TestCase
assert_equal(categories(:sub_special_3).class, SpecialCategory) assert_equal(categories(:sub_special_3).class, SpecialCategory)
end end
def test_binary_in_fixtures
assert_equal 1, @binaries.size
data = File.read(BINARY_FIXTURE_PATH).freeze
assert_equal data, @flowers.data
end
end end
if Account.connection.respond_to?(:reset_pk_sequence!) if Account.connection.respond_to?(:reset_pk_sequence!)

View file

@ -170,7 +170,7 @@ if ActiveRecord::Base.connection.supports_migrations?
# SQL Server and Sybase will not allow you to add a NOT NULL column # SQL Server and Sybase will not allow you to add a NOT NULL column
# to a table without specifying a default value, so the # to a table without specifying a default value, so the
# following test must be skipped # following test must be skipped
unless current_adapter?(:SQLServerAdapter, :SybaseAdapter) unless current_adapter?(:SQLServerAdapter, :SybaseAdapter, :SQLiteAdapter)
def test_add_column_not_null_without_default def test_add_column_not_null_without_default
Person.connection.create_table :testings do |t| Person.connection.create_table :testings do |t|
t.column :foo, :string t.column :foo, :string
@ -209,7 +209,7 @@ if ActiveRecord::Base.connection.supports_migrations?
def test_native_decimal_insert_manual_vs_automatic def test_native_decimal_insert_manual_vs_automatic
# SQLite3 always uses float in violation of SQL # SQLite3 always uses float in violation of SQL
# 16 decimal places # 16 decimal places
correct_value = (current_adapter?(:SQLiteAdapter) ? '0.123456789012346E20' : '0012345678901234567890.0123456789').to_d correct_value = '0012345678901234567890.0123456789'.to_d
Person.delete_all Person.delete_all
Person.connection.add_column "people", "wealth", :decimal, :precision => '30', :scale => '10' Person.connection.add_column "people", "wealth", :decimal, :precision => '30', :scale => '10'
@ -227,8 +227,9 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_kind_of BigDecimal, row.wealth assert_kind_of BigDecimal, row.wealth
# If this assert fails, that means the SELECT is broken! # If this assert fails, that means the SELECT is broken!
unless current_adapter?(:SQLite3Adapter)
assert_equal correct_value, row.wealth assert_equal correct_value, row.wealth
end
# Reset to old state # Reset to old state
Person.delete_all Person.delete_all
@ -240,8 +241,9 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_kind_of BigDecimal, row.wealth assert_kind_of BigDecimal, row.wealth
# If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken! # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken!
unless current_adapter?(:SQLite3Adapter)
assert_equal correct_value, row.wealth assert_equal correct_value, row.wealth
end
# Reset to old state # Reset to old state
Person.connection.del_column "people", "wealth" rescue nil Person.connection.del_column "people", "wealth" rescue nil
Person.reset_column_information Person.reset_column_information
@ -267,10 +269,7 @@ if ActiveRecord::Base.connection.supports_migrations?
# Test for 30 significent digits (beyond the 16 of float), 10 of them # Test for 30 significent digits (beyond the 16 of float), 10 of them
# after the decimal place. # after the decimal place.
if current_adapter?(:SQLiteAdapter) unless current_adapter?(:SQLite3Adapter)
# SQLite3 uses float in violation of SQL. Test for 16 decimal places.
assert_equal BigDecimal.new('0.123456789012346E20'), bob.wealth
else
assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth
end end

View file

@ -211,6 +211,53 @@ class ListTest < Test::Unit::TestCase
assert_equal [new2, new1, new3], ListMixin.find(:all, :conditions => 'parent_id IS NULL', :order => 'pos') assert_equal [new2, new1, new3], ListMixin.find(:all, :conditions => 'parent_id IS NULL', :order => 'pos')
end end
def test_remove_from_list_should_then_fail_in_list?
assert_equal true, mixins(:list_1).in_list?
mixins(:list_1).remove_from_list
assert_equal false, mixins(:list_1).in_list?
end
def test_remove_from_list_should_set_position_to_nil
assert_equal [mixins(:list_1),
mixins(:list_2),
mixins(:list_3),
mixins(:list_4)],
ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos')
mixins(:list_2).remove_from_list
assert_equal [mixins(:list_2, :reload),
mixins(:list_1, :reload),
mixins(:list_3, :reload),
mixins(:list_4, :reload)],
ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos')
assert_equal 1, mixins(:list_1).pos
assert_equal nil, mixins(:list_2).pos
assert_equal 2, mixins(:list_3).pos
assert_equal 3, mixins(:list_4).pos
end
def test_remove_before_destroy_does_not_shift_lower_items_twice
assert_equal [mixins(:list_1),
mixins(:list_2),
mixins(:list_3),
mixins(:list_4)],
ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos')
mixins(:list_2).remove_from_list
mixins(:list_2).destroy
assert_equal [mixins(:list_1, :reload),
mixins(:list_3, :reload),
mixins(:list_4, :reload)],
ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos')
assert_equal 1, mixins(:list_1).pos
assert_equal 2, mixins(:list_3).pos
assert_equal 3, mixins(:list_4).pos
end
end end
class TreeTest < Test::Unit::TestCase class TreeTest < Test::Unit::TestCase

View file

@ -631,7 +631,7 @@ class ValidationsTest < Test::Unit::TestCase
t = Topic.new('title' => 'noreplies', 'content' => 'whatever') t = Topic.new('title' => 'noreplies', 'content' => 'whatever')
assert !t.save assert !t.save
assert t.errors.on(:replies) assert t.errors.on(:replies)
t.replies.create('title' => 'areply', 'content' => 'whateveragain') t.replies.build('title' => 'areply', 'content' => 'whateveragain')
assert t.valid? assert t.valid?
end end
@ -824,7 +824,7 @@ class ValidationsTest < Test::Unit::TestCase
t = Topic.new('title' => 'あいうえお', 'content' => 'かきくけこ') t = Topic.new('title' => 'あいうえお', 'content' => 'かきくけこ')
assert !t.save assert !t.save
assert t.errors.on(:replies) assert t.errors.on(:replies)
t.replies.create('title' => 'あいうえお', 'content' => 'かきくけこ') t.replies.build('title' => 'あいうえお', 'content' => 'かきくけこ')
assert t.valid? assert t.valid?
end end
end end

View file

@ -1,3 +1,19 @@
*1.4.4* (October 12th, 2007)
* Backport: allow array and hash query parameters. Array route parameters are converted/to/a/path as before. #6765, #7047, #7462 [bgipsy, Jeremy McAnally, Dan Kubb, brendan, Diego Algorta Casamayou]
*1.4.3* (October 4th, 2007)
* Demote Hash#to_xml to use XmlSimple#xml_in_string so it can't read files or stdin. #8453 [candlerb, Jeremy Kemper]
* Document Object#blank?. #6491 [Chris Mear]
* Update Dependencies to ignore constants inherited from ancestors. Closes #6951. [Nicholas Seckar]
* Improved multibyte performance by relying less on exception raising #8159 [Blaine]
*1.4.2* (March 12th, 2007) *1.4.2* (March 12th, 2007)
* Ruby 1.8.6 and 1.9 define private Time#to_date and #to_datetime; make them * Ruby 1.8.6 and 1.9 define private Time#to_date and #to_datetime; make them

View file

@ -1,5 +1,11 @@
class Object #:nodoc: class Object
# "", " ", nil, [], and {} are blank # An object is blank if it's nil, empty, or a whitespace string.
# For example, "", " ", nil, [], and {} are blank.
#
# This simplifies
# if !address.nil? && !address.empty?
# to
# if !address.blank?
def blank? def blank?
if respond_to?(:empty?) && respond_to?(:strip) if respond_to?(:empty?) && respond_to?(:strip)
empty? or strip.empty? empty? or strip.empty?

View file

@ -1,5 +1,44 @@
require 'date' require 'date'
require 'xml_simple' require 'xml_simple'
require 'cgi'
# Extensions needed for Hash#to_query
class Object
def to_param #:nodoc:
to_s
end
def to_query(key) #:nodoc:
"#{CGI.escape(key.to_s)}=#{CGI.escape(to_param.to_s)}"
end
end
class Array
def to_query(key) #:nodoc:
collect { |value| value.to_query("#{key}[]") }.sort * '&'
end
end
# Locked down XmlSimple#xml_in_string
class XmlSimple
# Same as xml_in but doesn't try to smartly shoot itself in the foot.
def xml_in_string(string, options = nil)
handle_options('in', options)
@doc = parse(string)
result = collapse(@doc.root)
if @options['keeproot']
merge({}, @doc.root.name, result)
else
result
end
end
def self.xml_in_string(string, options = nil)
new.xml_in_string(string, options)
end
end
module ActiveSupport #:nodoc: module ActiveSupport #:nodoc:
module CoreExtensions #:nodoc: module CoreExtensions #:nodoc:
@ -27,6 +66,12 @@ module ActiveSupport #:nodoc:
klass.extend(ClassMethods) klass.extend(ClassMethods)
end end
def to_query(namespace = nil)
collect do |key, value|
value.to_query(namespace ? "#{namespace}[#{key}]" : key)
end.sort * '&'
end
def to_xml(options = {}) def to_xml(options = {})
options[:indent] ||= 2 options[:indent] ||= 2
options.reverse_merge!({ :builder => Builder::XmlMarkup.new(:indent => options[:indent]), options.reverse_merge!({ :builder => Builder::XmlMarkup.new(:indent => options[:indent]),
@ -81,7 +126,7 @@ module ActiveSupport #:nodoc:
module ClassMethods module ClassMethods
def from_xml(xml) def from_xml(xml)
# TODO: Refactor this into something much cleaner that doesn't rely on XmlSimple # TODO: Refactor this into something much cleaner that doesn't rely on XmlSimple
undasherize_keys(typecast_xml_value(XmlSimple.xml_in(xml, undasherize_keys(typecast_xml_value(XmlSimple.xml_in_string(xml,
'forcearray' => false, 'forcearray' => false,
'forcecontent' => true, 'forcecontent' => true,
'keeproot' => true, 'keeproot' => true,

View file

@ -18,4 +18,18 @@ class Module
parents << Object unless parents.include? Object parents << Object unless parents.include? Object
parents parents
end end
# Return the constants that have been defined locally by this object and not
# in an ancestor. This method may miss some constants if their definition in
# the ancestor is identical to their definition in the receiver.
def local_constants
inherited = {}
ancestors.each do |anc|
next if anc == self
anc.constants.each { |const| inherited[const] = anc.const_get(const) }
end
constants.select do |const|
! inherited.key?(const) || inherited[const].object_id != const_get(const).object_id
end
end
end end

View file

@ -318,13 +318,13 @@ module Dependencies #:nodoc:
watch_frames = descs.collect do |desc| watch_frames = descs.collect do |desc|
if desc.is_a? Module if desc.is_a? Module
mod_name = desc.name mod_name = desc.name
initial_constants = desc.constants initial_constants = desc.local_constants
elsif desc.is_a?(String) || desc.is_a?(Symbol) elsif desc.is_a?(String) || desc.is_a?(Symbol)
mod_name = desc.to_s mod_name = desc.to_s
# Handle the case where the module has yet to be defined. # Handle the case where the module has yet to be defined.
initial_constants = if qualified_const_defined?(mod_name) initial_constants = if qualified_const_defined?(mod_name)
mod_name.constantize.constants mod_name.constantize.local_constants
else else
[] []
end end
@ -349,7 +349,7 @@ module Dependencies #:nodoc:
mod = mod_name.constantize mod = mod_name.constantize
next [] unless mod.is_a? Module next [] unless mod.is_a? Module
new_constants = mod.constants - prior_constants new_constants = mod.local_constants - prior_constants
# Make sure no other frames takes credit for these constants. # Make sure no other frames takes credit for these constants.
constant_watch_stack.each do |frame_name, constants| constant_watch_stack.each do |frame_name, constants|

View file

@ -24,11 +24,13 @@ module ActiveSupport
"\r" => '\r', "\r" => '\r',
"\t" => '\t', "\t" => '\t',
'"' => '\"', '"' => '\"',
'\\' => '\\\\' '\\' => '\\\\',
'<' => '\\074',
'>' => '\\076'
} }
define_encoder String do |string| define_encoder String do |string|
'"' + string.gsub(/[\010\f\n\r\t"\\]/) { |s| '"' + string.gsub(/[\010\f\n\r\t"\\<>]/) { |s|
ESCAPED_CHARS[s] ESCAPED_CHARS[s]
}.gsub(/([\xC0-\xDF][\x80-\xBF]| }.gsub(/([\xC0-\xDF][\x80-\xBF]|
[\xE0-\xEF][\x80-\xBF]{2}| [\xE0-\xEF][\x80-\xBF]{2}|

Some files were not shown because too many files have changed in this diff Show more