commit 69b62b6f33bea9243f289d75d9ec8c6016b5d1cb Author: Jacques Distler Date: Mon Jan 22 07:43:50 2007 -0600 Checkout of Instiki Trunk 1/21/2007. diff --git a/CHANGELOG b/CHANGELOG new file mode 100755 index 00000000..a0c56d89 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,270 @@ + * TRUNK: + + - ANTISPAM: + - updated and included spam_patterns.txt + - included dnsbl_check - DNS Blackhole Lists check + [thanks to joost from http://www.spacebabies.nl ] + + - BUGFIXES: + - fix PDF output not to contain garbage chars [Jesse Newland] + - fixed the pages and authors display for single webs + - web list does not show a link to a published version if it has none + [Jesse Newland] + - Fixed bug that failed to expire cached diff view of a page + - Fixed rendering of WikiLinks in included pages in published or export + mode + - lots of small bugfixes and changes + + - UPDATES: + - Rails 1.2 tested and packaged with instiki + - updated RubyZip to 0.9.1 + - updated RedCloth to 3.0.4 + - updated packaged sqlite3-ruby to 1.2.0 + + - FEATURES: + - Stylesheet tweaks + - visual display if webs are pass-protected (div background) + - Linux packaging + +------------------------------------------------------------------------------ + * 0.11.0: + - SQL-based backend (ActiveRecord) + - File uploads (finally) + - Upgraded to Rails 1.0.0 + - Replaced internal link generator with routing + - Fixed --daemon option + - Removed Rubygem and native OS X distributions + - Improved HTML diff + - More accurate "See Changes" +------------------------------------------------------------------------------ + * 0.10.2: + - Upgraded to Rails 0.13.1 + - Fixed HTML export + - Added layout=no option to the export_html action (it exports page + contents processed by the markup engine, but without the default layout - + so that they can be wrapped in some other layout) + - tag can span several lines (before it was applied when both + opening and closing tags were on the same line only) + - Resolved the "endless redirection loop" condition and otherwise improved + handling of errors in the rendering engines + - Fixed rendering of Markdown hyperlinks such as + [Text](http://something.com/foo) +------------------------------------------------------------------------------ + * 0.10.1: + - Upgraded Rails to 0.12.0 + - Upgraded rubyzip to version 0.5.8 + - BlueCloth is back (RedCloth didn't do pure Markdown well enough) + - Handling of line breaks in Textile is as in 0.9 (inserts
tag) + - Fixed HTML export (to enclose the output in tags, include the + stylesheet etc) + - Corrected some compatibility issues with storages from earlier Instiki + versions + - Some other bug fixes +------------------------------------------------------------------------------ + * 0.10.0: + - Ported to ActionPack + - RedCloth 3.0.3 + - BlueCloth is phased out, Markdown is rendered by RedCloth + - Mix markup option understands both Textile and Markdown on the same page + - Instiki can serve static content (such as HTML or plain-text files) from + ./public directory + - Much friendlier admin interface + - Wiki link syntax doesn't conflict with Textile hyperlink syntax. + Therefore "textile link":LinkToSomePlace will not look insane. + - RSS feeds accept query parameters, such as + http://localhost:2500/wiki/rss_with_headlines?start=2005-02-18&end=2005-02-19&limit=10 + - RSS feed with page contents for a password-protected web behaves as + follows: if the web is published, RSS feed links to the published version + of the web. otherwise, the feed is not available + Madeleine will check every hour if there are new commands in the log or + 24 hours have passed since last snapshot, and take snapshot if either of + these conditions is true. Madeleine will also not log read-only + operations, resulting in a better performance + - Wiki extracts (to HTML and plain text) will leave only the last extract + file in ./storage + - Wiki search handles multibyte (UTF-8) characters correctly + - Local hyperlinks in published pages point to published pages [Michael + DeHaan] + - Fixed a bug that sometimes caused all past revisions of a page to be + "forgotten" on restart + - Fixed parsing of URIs with a port number (http://someplace.org:8080) + - Instiki will not fork itself on a *nix, unless explicitly asked to + - Instiki can bind to IPs other than 127.0.0.1 (command-line option) + - Revisions that do not change anything on the page are rejected + - Automated tests for all controller actions + - category: lines are presented as links to "All Pages" for relevant + categories + - Search looks at page titles, as well as content + - Multiple other usability enhancements and bug fixes +------------------------------------------------------------------------------ + * 0.9.2: + - Rollback takes the user to an edit form. The form has to be submitted for + the change to take place. + - Changed to use inline style on published pages + - Fixed "forward in time" on the last revision before current page + - Instiki won't log bogus error messages when creating a new Wiki + - Fixed deprecation warning for Object.id (introduced in Ruby 1.8.2) + - Madeleine upgraded to 0.7.1 + - Madeleine snapshots are compressed + - Packaged as a gem +------------------------------------------------------------------------------ + * 0.9.1: + - Added performance improvements for updating existing pages + - Fixed IP logging and RSS feeds behind proxies [With help from Guan Yang] + - Fixed default storage directory (borked running on Windows) + [Spotted by Curt Hibbs] +------------------------------------------------------------------------------ + * 0.9.0: + - Added aliased links such as [[HomePage|that nice home page]] [Mark Reid] + - Added include other page content with [[!include TableOfContents]] + [Mark Reid] + - Added delete orphan pages from the Edit Web screen [by inspiration from + Simon Arnaud] + - Added logging of IP address for authors (who's behind the rollback wars) + - Added Categories pages through backlinks (use "categories: news, instiki" + on start of line) [Mark Reid] + - Added option to use bracket-style wiki links only (and hence ban + WikiWords) + - Added command-line option to specify different storage path + - Added print view without navigation + - Added character and page (2275 characters including spaces) counter + (important for student papers) + - Off by default, activate it on the Edit Web screen + - Added LaTeX/PDF integration on Textile installations with pdflatex + installed on system (EXPERIMENTAL) + - Use the home page as a table of contents with a unordered list to control + sections + - Added limit of 15 to the number of pages included in RSS feed + - Moved static parts of stylesheet to separate file [Lau T?rnskov] + - Fixed better semantics for revision movement [Ryan Singer] + - Fixed color diffs to work much better [Xen/Mertz/Atkins] + - Fixed performance problems for All Pages list [Dennis Mertz] + - Fixed lots of rendering bugs [Mark Reid] + - Upgraded to RedCloth 2.0.11 [integrating the fine work of Dennis Mertz] +------------------------------------------------------------------------------ + * 0.8.9: + - Added color diffs to see changes between revisions [Bill Atkins] + They're aren't quite perfect yet as new paragraphs split the tags + (hence 0.8.9, not 0.9.0) + - Added redirect to edit if content of page generates an error + (so the page doesn't become unusable on bugs in the markup engines) + - Fixed update Web with different address bug [Denis Metz] + - Fixed a bunch of wiki word rendering issues by doing wiki word detection + and replacment at once + - Upgraded to BlueCloth 0.0.3b (should fix loads of problems on Markdown + wikis) +------------------------------------------------------------------------------ + * 0.8.5: + - Instiki can now serve as a CMS by running a password-protected web with a + published front + - Added version check at startup (Instiki needs Ruby 1.8.1) +------------------------------------------------------------------------------ + * 0.8.1: + - Actually included RedCloth 2.0.7 in the release +------------------------------------------------------------------------------ + * 0.8.0: + - NOTE: Single-web wikis created in versions prior to 0.8.0 have "instiki" + as their system password + - Accepts wiki words in bracket style. + Examples: [[wiki word]], [[c]], [[We could'nt have done it!]] + - Accepts camel-case wiki words in all latin, greek, cyrillian, and + armenian unicode characters + - Many thanks to Guan Yang for building the higher- and lower-case lookup + tables. And thanks to Simon Arnaud for the initial patch that got the + work started + - Changed charset to UTF-8 + - Cut down on command-line options and replaced them with an per-web config + screen + - Added option to extend the stylesheet on a per-web basis to tweak the + look in details + - Added simple color options for variety + - Added option to add/remove password protection on each web + - Added the wiki name of the author locking a given page (instead of just + "someone") + - Removed single/multi-web distinction -- all Instikis are now multi-web + - Load libraries from an unshifted load path, so that old installed + libraries doesn't clash [Emiel van de Laar] + - Keeps the author cookie forever, so you don't have to enter your name + again and again + - Fixed XHTML so it validates [Bruce D'Arcus] + - Authors are no longer listed under orphan pages + - Added export to markup (great for backups, potentially for switching wiki + engine) + - Don't link wiki words that proceeds from either /, = or ? + (http://c2.com/cgi/wiki?WikiWikiClones, + /show/HomePage, cgi.pl?show=WikiWord without escaping) + - Accessing an unexisting page redirects to a different url (/new/PageName) + - Increased snapshot time to just once a day (cuts down on disk storage + requirements) + - Made RDoc support work better with 1.8.1 [Mauricio Fern?ndez] + - Added convinient redirect from /wiki/ to /wiki/show/HomePage + - Fixed BlueCloth bug with backticks at start of line + - Updated to RedCloth 2.0.7 (and linked to the new Textile reference) +------------------------------------------------------------------------------ + * 0.7.0: + - Added Markdown (BlueCloth) and RDoc [Mauricio Fern?ndez] as command-line + markup choices + - Added wanted and orphan page lists to All pages (only show up if there's + actually orphan or wanted pages) + - Added ISO-8859-1 as XML encoding in RSS feeds (makes FeedReader among + others happy for special entities) + - Added proper links in the RSS feed (but the body links are still + relative, which NNW and others doesn't grok) + - Added access keys: E => Edit, H => HomePage, A => All Pages, + U => Recently Revised, X => Export + - Added password-login through URL (so you can subscribe to feed on a + protected web) + - Added web passwords to the feed links for protected webs, so they work + without manual login + - Added the web name in small letters above all pages within a web + - Polished authors and recently revised + - Updated to RedCloth 2.0.6 + - Changed content type for RSS feeds to text/xml (makes Mozilla Aggreg8 + happy) + - Changed searching to be case insensitive + - Changed HomePage to display the name of the web instead + - Changed exported HTML pages to be valid XHTML (which can be preprocessed + by XSLT) + - Fixed broken recently revised +------------------------------------------------------------------------------ + * 0.6.0: + - Fixed Windows compatibility [Florian] + - Fixed bug that would prevent Madeleine from taking snapshots in Daemon + mode + - Added export entire web as HTML in a zip file + - Added RSS feeds + - Added proper getops support for the growing number of options [Florian] + - Added safe mode that forbids style options in RedCloth [Florian] + - Updated RedCloth to 2.0.5 +------------------------------------------------------------------------------ + * 0.5.0: + - NOTE: 0.5.0 is NOT compatible with databases from earlier versions + - Added revisions + - Added multiple webs + - Added password protection for webs on multi-web setups + - Added the notion of authors (that are saved in a cookie) + - Added command-line option for not running as a Daemon on Unix +------------------------------------------------------------------------------ + * 0.3.1: + - Added option to escape wiki words with \ +------------------------------------------------------------------------------ + * 0.3.0: + - Brought all files into common style (including Textile help on the edit + page) + - Added page locking (if someone already is editing a page there's a + warning) + - Added daemon abilities on Unix (keep Instiki running after you close the + terminal) + - Made port 2500 the default port, so Instiki can be launched by + dobbelt-click + - Added Textile cache to speed-up rendering of large pages + - Made WikiWords look like "Wiki Words" + - Updated RedCloth to 2.0.4 +------------------------------------------------------------------------------ + * 0.2.5: + - Upgraded to RedCloth 2.0.2 and Madeleine 0.6.1, which means the + - Windows problems are gone. Also fixed a problem with wikiwords + - that used part of other wikiwords. +------------------------------------------------------------------------------ + * 0.2.0: + - First public release diff --git a/README b/README new file mode 100755 index 00000000..ffcab202 --- /dev/null +++ b/README @@ -0,0 +1,113 @@ +===What is Instiki? + +Admitted, it's YetAnotherWikiClone[http://c2.com/cgi/wiki?WikiWikiClones], but with a strong focus +on simplicity of installation and running: + +Step 1. Download +Step 2. Run "instiki" + +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 +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 +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" +4. Install SQLite 3 database engine from http://www.sqlite.org/ +5. Install SQLite 3 driver for Ruby from http://sqlite-ruby.rubyforge.org/ +6. Install Rake from http://rake.rubyforge.org/ +7. Execute rm -f db/*.db +8. Execute 'rake environment RAILS_ENV=production migrate' +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 +12. At least, there is no step twelve! (TM) + +===Features: +* Regular expression search: Find deep stuff really fast +* 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 +* RSS feeds to track recently revised pages +* Multiple webs: Create separate wikis with their own namespace +* Password-protected webs: Keep it private +* 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? +* Speed: Using Madelein[http://madeleine.sourceforge.net] for persistence (all pages are in memory) +* Three markup choices: Textile[http://www.textism.com/tools/textile] + (default / RedCloth[http://www.whytheluckystiff.net/ruby/redcloth]), + Markdown (BlueCloth[http://bluecloth.rubyforge.org]), and RDoc[http://rdoc.sourceforge.net/doc] +* Embedded webserver: Through WEBrick[http://www.webrick.org] +* Internationalization: Wiki words in any latin, greek, cyrillian, or armenian characters +* Color diffs: Track changes through revisions +* Definitely can run on SQLite and MySQL +* 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: +* Run "ruby instiki --help" + +===History: + * See CHANGELOG + +===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) +2. Execute + ruby script\import_storage \ + -t /full/path/to/instiki0.10/storage \ + -i /full/path/to/instiki0.10/installation \ + -d sqlite (or mysql, or postgres, depending on what you use) \ + -o instiki_import.sql + for example (Windows): + ruby script\import_storage -t c:\instiki-0.10.2\storage\2500 -i c:\instiki-0.10.2 -d sqlite -o instiki_import.sql +3. This will produce instiki_import.sql file in the current working directory. + Open it in a text editor and inspect carefully. +4. Connect to your production database (e.g., 'sqlite3 db\prod.db'), + and have it execute instiki_import.sql (e.g., '.read instiki_import.sql') +5. Execute ruby script\reset_references + (this script parses all pages for crosslinks between them, so it may take a few minutes) +6. Restart Instiki +7. Go over some pages, especially those with a lot of complex markup, and see if anything is broken. + +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. + +===Upgrading from Instiki-AR Beta 1 +In Beta 2, we switch to ActiveRecord:Migrations. Therefore: +1. Back up your production database. +2. Open command-line session to your database and execute: + create table schema_info (version integer(11)); + insert into schema_info (version) values (1); +3. Go back to the shell, change directory to the new Instiki and execute "rake migrate". + +Step 2 creates a table that tells to ActiveRecord:Migrations that the current version +of this database is 1 (corresponding to Beta 1), and step 3 makes it up-to-date with +the current version of Instiki. + +===Download the latest release from: +* http://rubyforge.org/project/showfiles.php?group_id=186 + +===Visit the "official" Instiki wiki: +* http://instiki.org + +===License: +* same as Ruby's + +--- +Authors:: + +Versions 0.0 to 0.9.1:: David Heinemeier Hansson +Email:: david@loudthinking.com +Weblog:: http://www.loudthinking.com + +From 0.9.2 onwards:: Alexey Verkhovsky +Email:: alex@verk.info diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb new file mode 100644 index 00000000..4390359d --- /dev/null +++ b/app/controllers/admin_controller.rb @@ -0,0 +1,94 @@ +require 'application' + +class AdminController < ApplicationController + + layout 'default' + cache_sweeper :web_sweeper + + def create_system + if @wiki.setup? + flash[:error] = + "Wiki has already been created in '#{@wiki.storage_path}'. " + + "Shut down Instiki and delete this directory if you want to recreate it from scratch." + + "\n\n" + + "(WARNING: this will destroy content of your current wiki)." + redirect_home(@wiki.webs.keys.first) + elsif @params['web_name'] + # form submitted -> create a wiki + @wiki.setup(@params['password'], @params['web_name'], @params['web_address']) + flash[:info] = "Your new wiki '#{@params['web_name']}' is created!\n" + + "Please edit its home page and press Submit when finished." + redirect_to :web => @params['web_address'], :controller => 'wiki', :action => 'new', + :id => 'HomePage' + else + # no form submitted -> go to template + end + end + + def create_web + if @params['address'] + # form submitted + if @wiki.authenticate(@params['system_password']) + begin + @wiki.create_web(@params['name'], @params['address']) + flash[:info] = "New web '#{@params['name']}' successfully created." + redirect_to :web => @params['address'], :controller => 'wiki', :action => 'new', + :id => 'HomePage' + rescue Instiki::ValidationError => e + @error = e.message + # and re-render the form again + end + else + redirect_to :controller => 'wiki', :action => 'index' + end + else + # no form submitted -> render template + end + end + + def edit_web + system_password = @params['system_password'] + if system_password + # form submitted + if wiki.authenticate(system_password) + begin + wiki.edit_web( + @web.address, @params['address'], @params['name'], + @params['markup'].intern, + @params['color'], @params['additional_style'], + @params['safe_mode'] ? true : false, + @params['password'].empty? ? nil : @params['password'], + @params['published'] ? true : false, + @params['brackets_only'] ? true : false, + @params['count_pages'] ? true : false, + @params['allow_uploads'] ? true : false, + @params['max_upload_size'] + ) + flash[:info] = "Web '#{@params['address']}' was successfully updated" + redirect_home(@params['address']) + rescue Instiki::ValidationError => e + logger.warn e.message + @error = e.message + # and re-render the same template again + end + else + @error = password_error(system_password) + # and re-render the same template again + end + else + # no form submitted - go to template + end + end + + def remove_orphaned_pages + if wiki.authenticate(@params['system_password_orphaned']) + wiki.remove_orphaned_pages(@web_name) + flash[:info] = 'Orphaned pages removed' + redirect_to :controller => 'wiki', :web => @web_name, :action => 'list' + else + flash[:error] = password_error(@params['system_password_orphaned']) + redirect_to :controller => 'admin', :web => @web_name, :action => 'edit_web' + end + end + +end diff --git a/app/controllers/application.rb b/app/controllers/application.rb new file mode 100644 index 00000000..59c43eb0 --- /dev/null +++ b/app/controllers/application.rb @@ -0,0 +1,190 @@ +# The filters added to this controller will be run for all controllers in the application. +# Likewise will all the methods added be available for all controllers. +class ApplicationController < ActionController::Base +# require 'dnsbl_check' + before_filter :dnsbl_check, :connect_to_model, :check_authorization, :setup_url_generator, :set_content_type_header, :set_robots_metatag + after_filter :remember_location, :teardown_url_generator + + # For injecting a different wiki model implementation. Intended for use in tests + def self.wiki=(the_wiki) + # a global variable is used here because Rails reloads controller and model classes in the + # development environment; therefore, storing it as a class variable does not work + # class variable is, anyway, not much different from a global variable + #$instiki_wiki_service = the_wiki + logger.debug("Wiki service: #{the_wiki.to_s}") + end + + def self.wiki + Wiki.new + end + + protected + + def check_authorization + if in_a_web? and authorization_needed? and not authorized? + redirect_to :controller => 'wiki', :action => 'login', :web => @web_name + return false + end + end + + def connect_to_model + @action_name = @params['action'] || 'index' + @web_name = @params['web'] + @wiki = wiki + @author = cookies['author'] || 'AnonymousCoward' + if @web_name + @web = @wiki.webs[@web_name] + if @web.nil? + render(:status => 404, :text => "Unknown web '#{@web_name}'") + return false + end + end + end + + FILE_TYPES = { + '.exe' => 'application/octet-stream', + '.gif' => 'image/gif', + '.jpg' => 'image/jpeg', + '.pdf' => 'application/pdf', + '.png' => 'image/png', + '.txt' => 'text/plain', + '.zip' => 'application/zip' + } unless defined? FILE_TYPES + + DISPOSITION = { + 'application/octet-stream' => 'attachment', + 'image/gif' => 'inline', + 'image/jpeg' => 'inline', + 'application/pdf' => 'inline', + 'image/png' => 'inline', + 'text/plain' => 'inline', + 'application/zip' => 'attachment' + } unless defined? DISPOSITION + + def determine_file_options_for(file_name, original_options = {}) + original_options[:type] ||= (FILE_TYPES[File.extname(file_name)] or 'application/octet-stream') + original_options[:disposition] ||= (DISPOSITION[original_options[:type]] or 'attachment') + original_options[:stream] ||= false + original_options + end + + def send_file(file, options = {}) + determine_file_options_for(file, options) + super(file, options) + end + + def password_check(password) + if password == @web.password + cookies['web_address'] = password + true + else + false + end + end + + def password_error(password) + if password.nil? or password.empty? + 'Please enter the password.' + else + 'You entered a wrong password. Please enter the right one.' + end + end + + def redirect_home(web = @web_name) + if web + redirect_to_page('HomePage', web) + else + redirect_to_url '/' + end + end + + def redirect_to_page(page_name = @page_name, web = @web_name) + redirect_to :web => web, :controller => 'wiki', :action => 'show', + :id => (page_name or 'HomePage') + end + + def remember_location + if @request.method == :get and + @response.headers['Status'] == '200 OK' and not + %w(locked save back file pic import).include?(action_name) + @session[:return_to] = @request.request_uri + logger.debug "Session ##{session.object_id}: remembered URL '#{@session[:return_to]}'" + end + end + + def rescue_action_in_public(exception) + render :status => 500, :text => <<-EOL + +

Internal Error

+

An application error occurred while processing your request.

+ + + EOL + end + + def return_to_last_remembered + # Forget the redirect location + redirect_target, @session[:return_to] = @session[:return_to], nil + tried_home, @session[:tried_home] = @session[:tried_home], false + + # then try to redirect to it + if redirect_target.nil? + if tried_home + raise 'Application could not render the index page' + else + logger.debug("Session ##{session.object_id}: no remembered redirect location, trying home") + redirect_home + end + else + logger.debug("Session ##{session.object_id}: " + + "redirect to the last remembered URL #{redirect_target}") + redirect_to_url(redirect_target) + end + end + + def set_content_type_header + if %w(rss_with_content rss_with_headlines).include?(action_name) + @response.headers['Content-Type'] = 'text/xml; charset=UTF-8' + else + @response.headers['Content-Type'] = 'text/html; charset=UTF-8' + end + end + + def set_robots_metatag + if controller_name == 'wiki' and %w(show published).include? action_name + @robots_metatag_value = 'index,follow' + else + @robots_metatag_value = 'noindex,nofollow' + end + end + + def setup_url_generator + PageRenderer.setup_url_generator(UrlGenerator.new(self)) + end + + def teardown_url_generator + PageRenderer.teardown_url_generator + end + + def wiki + self.class.wiki + end + + private + + def in_a_web? + not @web_name.nil? + end + + def authorization_needed? + not %w( login authenticate published rss_with_content rss_with_headlines ).include?(action_name) + end + + def authorized? + @web.nil? or + @web.password.nil? or + cookies['web_address'] == @web.password or + password_check(@params['password']) + end + +end diff --git a/app/controllers/cache_sweeping_helper.rb b/app/controllers/cache_sweeping_helper.rb new file mode 100644 index 00000000..2bf3e9f8 --- /dev/null +++ b/app/controllers/cache_sweeping_helper.rb @@ -0,0 +1,23 @@ +module CacheSweepingHelper + + def expire_cached_page(web, page_name) + expire_action :controller => 'wiki', :web => web.address, + :action => %w(show published), :id => page_name + expire_action :controller => 'wiki', :web => web.address, + :action => %w(show published), :id => page_name, :mode => 'diff' + end + + def expire_cached_summary_pages(web) + categories = WikiReference.find(:all, :conditions => "link_type = 'C'") + %w(recently_revised list).each do |action| + expire_action :controller => 'wiki', :web => web.address, :action => action + categories.each do |category| + expire_action :controller => 'wiki', :web => web.address, :action => action, :category => category.referenced_name + end + end + + expire_action :controller => 'wiki', :web => web.address, :action => 'authors' + expire_fragment :controller => 'wiki', :web => web.address, :action => %w(rss_with_headlines rss_with_content) + end + +end \ No newline at end of file diff --git a/app/controllers/file_controller.rb b/app/controllers/file_controller.rb new file mode 100644 index 00000000..865450aa --- /dev/null +++ b/app/controllers/file_controller.rb @@ -0,0 +1,100 @@ +# Controller responsible for serving files and pictures. + +require 'zip/zip' + +class FileController < ApplicationController + + layout 'default' + + before_filter :check_allow_uploads + + def file + @file_name = params['id'] + if @params['file'] + # form supplied + new_file = @web.wiki_files.create(@params['file']) + if new_file.valid? + flash[:info] = "File '#{@file_name}' successfully uploaded" + return_to_last_remembered + else + # pass the file with errors back into the form + @file = new_file + render + end + else + # no form supplied, this is a request to download the file + file = WikiFile.find_by_file_name(@file_name) + if file + send_data(file.content, determine_file_options_for(@file_name, :filename => @file_name)) + else + @file = WikiFile.new(:file_name => @file_name) + render + end + end + end + + def cancel_upload + return_to_last_remembered + end + + def import + if @params['file'] + @problems = [] + import_file_name = "#{@web.address}-import-#{Time.now.strftime('%Y-%m-%d-%H-%M-%S')}.zip" + import_from_archive(@params['file'].path) + if @problems.empty? + flash[:info] = 'Import successfully finished' + else + flash[:error] = 'Import finished, but some pages were not imported:
  • ' + + @problems.join('
  • ') + '
  • ' + end + return_to_last_remembered + else + # to template + end + end + + protected + + def check_allow_uploads + render(:status => 404, :text => "Web #{@params['web'].inspect} not found") and return false unless @web + if @web.allow_uploads? + return true + else + render :status => 403, :text => 'File uploads are blocked by the webmaster' + return false + end + end + + private + + def import_from_archive(archive) + logger.info "Importing pages from #{archive}" + zip = Zip::ZipInputStream.open(archive) + while (entry = zip.get_next_entry) do + ext_length = File.extname(entry.name).length + page_name = entry.name[0..-(ext_length + 1)] + page_content = entry.get_input_stream.read + logger.info "Processing page '#{page_name}'" + begin + existing_page = @wiki.read_page(@web.address, page_name) + if existing_page + if existing_page.content == page_content + logger.info "Page '#{page_name}' with the same content already exists. Skipping." + next + else + logger.info "Page '#{page_name}' already exists. Adding a new revision to it." + wiki.revise_page(@web.address, page_name, page_content, Time.now, @author, PageRenderer.new) + end + else + wiki.write_page(@web.address, page_name, page_content, Time.now, @author, PageRenderer.new) + end + rescue => e + logger.error(e) + @problems << "#{page_name} : #{e.message}" + end + end + logger.info "Import from #{archive} finished" + end + +end diff --git a/app/controllers/revision_sweeper.rb b/app/controllers/revision_sweeper.rb new file mode 100644 index 00000000..1db2d2c6 --- /dev/null +++ b/app/controllers/revision_sweeper.rb @@ -0,0 +1,29 @@ +require_dependency 'cache_sweeping_helper' + +class RevisionSweeper < ActionController::Caching::Sweeper + + include CacheSweepingHelper + + observe Revision, Page + + def after_save(record) + if record.is_a?(Revision) + expire_caches(record.page) + end + end + + def after_delete(record) + if record.is_a?(Page) + expire_caches(record) + end + end + + private + + def expire_caches(page) + expire_cached_summary_pages(page.web) + pages_to_expire = ([page.name] + WikiReference.pages_that_reference(page.name)).uniq + pages_to_expire.each { |page_name| expire_cached_page(page.web, page_name) } + end + +end diff --git a/app/controllers/web_sweeper.rb b/app/controllers/web_sweeper.rb new file mode 100644 index 00000000..38e8f3da --- /dev/null +++ b/app/controllers/web_sweeper.rb @@ -0,0 +1,14 @@ +require_dependency 'cache_sweeping_helper' + +class WebSweeper < ActionController::Caching::Sweeper + + include CacheSweepingHelper + + observe Web + + def after_save(record) + web = record + web.pages.each { |page| expire_cached_page(web, page.name) } + expire_cached_summary_pages(web) + end +end diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb new file mode 100644 index 00000000..6bf52f7c --- /dev/null +++ b/app/controllers/wiki_controller.rb @@ -0,0 +1,429 @@ +require 'fileutils' +require 'redcloth_for_tex' +require 'parsedate' +require 'zip/zip' + +class WikiController < ApplicationController + + before_filter :load_page + caches_action :show, :published, :authors, :recently_revised, :list + cache_sweeper :revision_sweeper + + layout 'default', :except => [:rss_feed, :rss_with_content, :rss_with_headlines, :tex, :export_tex, :export_html] + + def index + if @web_name + redirect_home + elsif not @wiki.setup? + redirect_to :controller => 'admin', :action => 'create_system' + elsif @wiki.webs.length == 1 + redirect_home @wiki.webs.values.first.address + else + redirect_to :action => 'web_list' + end + end + + # Outside a single web -------------------------------------------------------- + + def authenticate + if password_check(@params['password']) + redirect_home + else + flash[:info] = password_error(@params['password']) + redirect_to :action => 'login', :web => @web_name + end + end + + def login + # to template + end + + def web_list + @webs = wiki.webs.values.sort_by { |web| web.name } + end + + + # Within a single web --------------------------------------------------------- + + def authors + @page_names_by_author = @web.page_names_by_author + @authors = @page_names_by_author.keys.sort + end + + def export_html + stylesheet = File.read(File.join(RAILS_ROOT, 'public', 'stylesheets', 'instiki.css')) + export_pages_as_zip('html') do |page| + + renderer = PageRenderer.new(page.revisions.last) + rendered_page = <<-EOL + + + + #{page.plain_name} in #{@web.name} + + + + + + + #{renderer.display_content_for_export} + + + + EOL + rendered_page + end + end + + def export_markup + export_pages_as_zip(@web.markup) { |page| page.content } + end + + def export_pdf + file_name = "#{@web.address}-tex-#{@web.revised_at.strftime('%Y-%m-%d-%H-%M-%S')}" + file_path = File.join(@wiki.storage_path, file_name) + + export_web_to_tex "#{file_path}.tex" unless FileTest.exists? "#{file_path}.tex" + convert_tex_to_pdf "#{file_path}.tex" + send_file "#{file_path}.pdf" + end + + def export_tex + file_name = "#{@web.address}-tex-#{@web.revised_at.strftime('%Y-%m-%d-%H-%M-%S')}.tex" + file_path = File.join(@wiki.storage_path, file_name) + export_web_to_tex(file_path) unless FileTest.exists?(file_path) + send_file file_path + end + + def feeds + @rss_with_content_allowed = rss_with_content_allowed? + # show the template + end + + def list + parse_category + @page_names_that_are_wanted = @pages_in_category.wanted_pages + @pages_that_are_orphaned = @pages_in_category.orphaned_pages + end + + def recently_revised + parse_category + @pages_by_revision = @pages_in_category.by_revision + @pages_by_day = Hash.new { |h, day| h[day] = [] } + @pages_by_revision.each do |page| + day = Date.new(page.revised_at.year, page.revised_at.month, page.revised_at.day) + @pages_by_day[day] << page + end + end + + def rss_with_content + if rss_with_content_allowed? + render_rss(hide_description = false, *parse_rss_params) + else + render_text 'RSS feed with content for this web is blocked for security reasons. ' + + 'The web is password-protected and not published', '403 Forbidden' + end + end + + def rss_with_headlines + render_rss(hide_description = true, *parse_rss_params) + end + + def search + @query = @params['query'] + @title_results = @web.select { |page| page.name =~ /#{@query}/i }.sort + @results = @web.select { |page| page.content =~ /#{@query}/i }.sort + all_pages_found = (@results + @title_results).uniq + if all_pages_found.size == 1 + redirect_to_page(all_pages_found.first.name) + end + end + + # Within a single page -------------------------------------------------------- + + def cancel_edit + @page.unlock + redirect_to_page(@page_name) + end + + def edit + if @page.nil? + redirect_home + elsif @page.locked?(Time.now) and not @params['break_lock'] + redirect_to :web => @web_name, :action => 'locked', :id => @page_name + else + @page.lock(Time.now, @author) + end + end + + def locked + # to template + end + + def new + # to template + end + + def pdf + page = wiki.read_page(@web_name, @page_name) + safe_page_name = @page.name.gsub(/\W/, '') + file_name = "#{safe_page_name}-#{@web.address}-#{@page.revised_at.strftime('%Y-%m-%d-%H-%M-%S')}" + file_path = File.join(@wiki.storage_path, file_name) + + export_page_to_tex("#{file_path}.tex") unless FileTest.exists?("#{file_path}.tex") + # NB: this is _very_ slow + convert_tex_to_pdf("#{file_path}.tex") + send_file "#{file_path}.pdf" + end + + def print + if @page.nil? + redirect_home + end + @link_mode ||= :show + @renderer = PageRenderer.new(@page.revisions.last) + # to template + end + + def published + if not @web.published? + render(:text => "Published version of web '#{@web_name}' is not available", :status => 404) + return + end + + @page_name ||= 'HomePage' + @page ||= wiki.read_page(@web_name, @page_name) + render(:text => "Page '#{@page_name}' not found", :status => 404) and return unless @page + + @renderer = PageRenderer.new(@page.revisions.last) + end + + def revision + get_page_and_revision + @show_diff = (@params[:mode] == 'diff') + @renderer = PageRenderer.new(@revision) + end + + def rollback + get_page_and_revision + end + + def save + render(:status => 404, :text => 'Undefined page name') and return if @page_name.nil? + + author_name = @params['author'] + author_name = 'AnonymousCoward' if author_name =~ /^\s*$/ + cookies['author'] = { :value => author_name, :expires => Time.utc(2030) } + + begin + filter_spam(@params['content']) + if @page + wiki.revise_page(@web_name, @page_name, @params['content'], Time.now, + Author.new(author_name, remote_ip), PageRenderer.new) + @page.unlock + else + wiki.write_page(@web_name, @page_name, @params['content'], Time.now, + Author.new(author_name, remote_ip), PageRenderer.new) + end + redirect_to_page @page_name + rescue => e + flash[:error] = e + logger.error e + flash[:content] = @params['content'] + if @page + @page.unlock + redirect_to :action => 'edit', :web => @web_name, :id => @page_name + else + redirect_to :action => 'new', :web => @web_name, :id => @page_name + end + end + end + + def show + if @page + begin + @renderer = PageRenderer.new(@page.revisions.last) + @show_diff = (@params[:mode] == 'diff') + render_action 'page' + # TODO this rescue should differentiate between errors due to rendering and errors in + # the application itself (for application errors, it's better not to rescue the error at all) + rescue => e + logger.error e + flash[:error] = e.message + if in_a_web? + redirect_to :action => 'edit', :web => @web_name, :id => @page_name + else + raise e + end + end + else + if not @page_name.nil? and not @page_name.empty? + redirect_to :web => @web_name, :action => 'new', :id => @page_name + else + render_text 'Page name is not specified', '404 Not Found' + end + end + end + + def tex + @tex_content = RedClothForTex.new(@page.content).to_tex + end + + protected + + def load_page + @page_name = @params['id'] + @page = @wiki.read_page(@web_name, @page_name) if @page_name + end + + private + + def convert_tex_to_pdf(tex_path) + # TODO remove earlier PDF files with the same prefix + # TODO handle gracefully situation where pdflatex is not available + begin + wd = Dir.getwd + Dir.chdir(File.dirname(tex_path)) + logger.info `pdflatex --interaction=nonstopmode #{File.basename(tex_path)}` + ensure + Dir.chdir(wd) + end + end + + def export_page_to_tex(file_path) + tex + File.open(file_path, 'w') { |f| f.write(render_to_string(:template => 'wiki/tex', :layout => false)) } + end + + def export_pages_as_zip(file_type, &block) + + file_prefix = "#{@web.address}-#{file_type}-" + timestamp = @web.revised_at.strftime('%Y-%m-%d-%H-%M-%S') + file_path = File.join(@wiki.storage_path, file_prefix + timestamp + '.zip') + tmp_path = "#{file_path}.tmp" + + Zip::ZipOutputStream.open(tmp_path) do |zip_out| + @web.select.by_name.each do |page| + zip_out.put_next_entry("#{CGI.escape(page.name)}.#{file_type}") + zip_out.puts(block.call(page)) + end + # add an index file, if exporting to HTML + if file_type.to_s.downcase == 'html' + zip_out.put_next_entry 'index.html' + zip_out.puts "" + + "" + end + end + FileUtils.rm_rf(Dir[File.join(@wiki.storage_path, file_prefix + '*.zip')]) + FileUtils.mv(tmp_path, file_path) + send_file file_path + end + + def export_web_to_tex(file_path) + @tex_content = table_of_contents(@web.page('HomePage').content, render_tex_web) + File.open(file_path, 'w') { |f| f.write(render_to_string(:template => 'wiki/tex_web', :layout => nil)) } + end + + def get_page_and_revision + if @params['rev'] + @revision_number = @params['rev'].to_i + else + @revision_number = @page.revisions.length + end + @revision = @page.revisions[@revision_number - 1] + end + + def parse_category + @categories = WikiReference.list_categories.sort + @category = @params['category'] + if @category + @set_name = "category '#{@category}'" + pages = WikiReference.pages_in_category(@category).sort.map { |page_name| @web.page(page_name) } + @pages_in_category = PageSet.new(@web, pages) + else + # no category specified, return all pages of the web + @pages_in_category = @web.select_all.by_name + @set_name = 'the web' + end + end + + def parse_rss_params + if @params.include? 'limit' + limit = @params['limit'].to_i rescue nil + limit = nil if limit == 0 + else + limit = 15 + end + start_date = Time.local(*ParseDate::parsedate(@params['start'])) rescue nil + end_date = Time.local(*ParseDate::parsedate(@params['end'])) rescue nil + [ limit, start_date, end_date ] + end + + def remote_ip + ip = @request.remote_ip + logger.info(ip) + ip + end + + def render_rss(hide_description = false, limit = 15, start_date = nil, end_date = nil) + if limit && !start_date && !end_date + @pages_by_revision = @web.select.by_revision.first(limit) + else + @pages_by_revision = @web.select.by_revision + @pages_by_revision.reject! { |page| page.revised_at < start_date } if start_date + @pages_by_revision.reject! { |page| page.revised_at > end_date } if end_date + end + + @hide_description = hide_description + @link_action = @web.password ? 'published' : 'show' + + render :action => 'rss_feed' + end + + def render_tex_web + @web.select.by_name.inject({}) do |tex_web, page| + tex_web[page.name] = RedClothForTex.new(page.content).to_tex + tex_web + end + end + + def rss_with_content_allowed? + @web.password.nil? or @web.published? + end + + def truncate(text, length = 30, truncate_string = '...') + if text.length > length then text[0..(length - 3)] + truncate_string else text end + end + + def filter_spam(content) + @@spam_patterns ||= load_spam_patterns + @@spam_patterns.each do |pattern| + raise "Your edit was blocked by spam filtering" if content =~ pattern + end + end + + def load_spam_patterns + spam_patterns_file = "#{RAILS_ROOT}/config/spam_patterns.txt" + if File.exists?(spam_patterns_file) + File.readlines(spam_patterns_file).inject([]) { |patterns, line| patterns << Regexp.new(line.chomp, Regexp::IGNORECASE) } + else + [] + end + end + + +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 00000000..67d7ae86 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,93 @@ +# The methods added to this helper will be available to all templates in the application. +module ApplicationHelper + + # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container + # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and + # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values + # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. + # + # Examples (call, result): + # html_options([["Dollar", "$"], ["Kroner", "DKK"]]) + # \n + # + # html_options([ "VISA", "Mastercard" ], "Mastercard") + # \n + # + # html_options({ "Basic" => "$20", "Plus" => "$40" }, "$40") + # \n + def html_options(container, selected = nil) + container = container.to_a if Hash === container + + html_options = container.inject([]) do |options, element| + if element.is_a? Array + if element.last != selected + options << "" + else + options << "" + end + else + options << ((element != selected) ? "" : "") + end + end + + html_options.join("\n") + end + + # Creates a hyperlink to a Wiki page, without checking if the page exists or not + def link_to_existing_page(page, text = nil, html_options = {}) + link_to( + text || page.plain_name, + {:web => @web.address, :action => 'show', :id => page.name, :only_path => true}, + html_options) + end + + # Creates a hyperlink to a Wiki page, or to a "new page" form if the page doesn't exist yet + def link_to_page(page_name, web = @web, text = nil, options = {}) + raise 'Web not defined' if web.nil? + UrlGenerator.new(@controller).make_link(page_name, web, text, + options.merge(:base_url => "#{base_url}/#{web.address}")) + end + + def author_link(page, options = {}) + UrlGenerator.new(@controller).make_link(page.author.name, page.web, nil, options) + end + + def base_url + home_page_url = url_for :controller => 'admin', :action => 'create_system', :only_path => true + home_page_url.sub(%r-/create_system/?$-, '') + end + + # Creates a menu of categories + def categories_menu + if @categories.empty? + '' + else + "
    \n" + + 'Categories:' + + '[' + link_to_unless_current('Any', :web => @web.address, :action => @action_name) + "]\n" + + @categories.map { |c| + link_to_unless_current(c, :web => @web.address, :action => @action_name, :category => c) + }.join(', ') + "\n" + + '
    ' + end + end + + # Performs HTML escaping on text, but keeps linefeeds intact (by replacing them with
    ) + def escape_preserving_linefeeds(text) + h(text).gsub(/\n/, '
    ') + end + + def format_date(date, include_time = true) + # Must use DateTime because Time doesn't support %e on at least some platforms + if include_time + DateTime.new(date.year, date.mon, date.day, date.hour, date.min, date.sec).strftime("%B %e, %Y %H:%M:%S") + else + DateTime.new(date.year, date.mon, date.day).strftime("%B %e, %Y") + end + end + + def rendered_content(page) + PageRenderer.new(page.revisions.last).display_content + end + +end diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb new file mode 100644 index 00000000..a5b97f29 --- /dev/null +++ b/app/helpers/wiki_helper.rb @@ -0,0 +1,89 @@ +module WikiHelper + + def navigation_menu_for_revision + menu = [] + menu << forward + menu << back_for_revision if @revision_number > 1 + menu << current_revision + menu << see_or_hide_changes_for_revision if @revision_number > 1 + menu << rollback + menu + end + + def navigation_menu_for_page + menu = [] + menu << edit_page + menu << edit_web if @page.name == "HomePage" + if @page.revisions.length > 1 + menu << back_for_page + menu << see_or_hide_changes_for_page + end + menu + end + + def edit_page + link_text = (@page.name == "HomePage" ? 'Edit Page' : 'Edit') + link_to(link_text, {:web => @web.address, :action => 'edit', :id => @page.name}, + {:class => 'navlink', :accesskey => 'E', :name => 'edit'}) + end + + def edit_web + link_to('Edit Web', {:web => @web.address, :action => 'edit_web'}, + {:class => 'navlink', :accesskey => 'W', :name => 'edit_web'}) + end + + def forward + if @revision_number < @page.revisions.length - 1 + link_to('Forward in time', + {:web => @web.address, :action => 'revision', :id => @page.name, :rev => @revision_number + 1}, + {:class => 'navlink', :accesskey => 'F', :name => 'to_next_revision'}) + + " (#{@revision.page.revisions.length - @revision_number} more) " + else + link_to('Forward in time', {:web => @web.address, :action => 'show', :id => @page.name}, + {:class => 'navlink', :accesskey => 'F', :name => 'to_next_revision'}) + + " (to current)" + end + end + + def back_for_revision + link_to('Back in time', + {:web => @web.address, :action => 'revision', :id => @page.name, :rev => @revision_number - 1}, + {:class => 'navlink', :name => 'to_previous_revision'}) + + " (#{@revision_number - 1} more)" + end + + def back_for_page + link_to('Back in time', + {:web => @web.address, :action => 'revision', :id => @page.name, + :rev => @page.revisions.length - 1}, + {:class => 'navlink', :accesskey => 'B', :name => 'to_previous_revision'}) + + " (#{@page.revisions.length - 1} #{@page.revisions.length - 1 == 1 ? 'revision' : 'revisions'})" + end + + def current_revision + link_to('See current', {:web => @web.address, :action => 'show', :id => @page.name}, + {:class => 'navlink', :name => 'to_current_revision'}) + end + + def see_or_hide_changes_for_revision + link_to(@show_diff ? 'Hide changes' : 'See changes', + {:web => @web.address, :action => 'revision', :id => @page.name, :rev => @revision_number, + :mode => (@show_diff ? nil : 'diff') }, + {:class => 'navlink', :accesskey => 'C', :name => 'see_changes'}) + end + + def see_or_hide_changes_for_page + link_to(@show_diff ? 'Hide changes' : 'See changes', + {:web => @web.address, :action => 'show', :id => @page.name, :mode => (@show_diff ? nil : 'diff') }, + {:class => 'navlink', :accesskey => 'C', :name => 'see_changes'}) + end + + def rollback + link_to('Rollback', + {:web => @web.address, :action => 'rollback', :id => @page.name, :rev => @revision_number}, + {:class => 'navlink', :name => 'rollback'}) + end + + + +end \ No newline at end of file diff --git a/app/models/author.rb b/app/models/author.rb new file mode 100644 index 00000000..be8a5cf7 --- /dev/null +++ b/app/models/author.rb @@ -0,0 +1,18 @@ +class Author < String + attr_accessor :ip + attr_reader :name + def initialize(name, ip = nil) + @ip = ip + super(name) + end + + def name=(value) + self.gsub!(/.+/, value) + end + + alias_method :name, :to_s + + def <=>(other) + name <=> other.to_s + end +end \ No newline at end of file diff --git a/app/models/page.rb b/app/models/page.rb new file mode 100644 index 00000000..c5f48d43 --- /dev/null +++ b/app/models/page.rb @@ -0,0 +1,121 @@ +class Page < ActiveRecord::Base + belongs_to :web + has_many :revisions, :order => 'id' + has_many :wiki_references, :order => 'referenced_name' + has_one :current_revision, :class_name => 'Revision', :order => 'id DESC' + + def revise(content, time, author, renderer) + revisions_size = new_record? ? 0 : revisions.size + if (revisions_size > 0) and content == current_revision.content + raise Instiki::ValidationError.new( + "You have tried to save page '#{name}' without changing its content") + end + + author = Author.new(author.to_s) unless author.is_a?(Author) + + # Try to render content to make sure that markup engine can take it, + renderer.revision = Revision.new( + :page => self, :content => content, :author => author, :revised_at => time) + renderer.display_content(update_references = true) + + # A user may change a page, look at it and make some more changes - several times. + # Not to record every such iteration as a new revision, if the previous revision was done + # by the same author, not more than 30 minutes ago, then update the last revision instead of + # creating a new one + if (revisions_size > 0) && continous_revision?(time, author) + current_revision.update_attributes(:content => content, :revised_at => time) + else + revisions.create(:content => content, :author => author, :revised_at => time) + end + save + self + end + + def rollback(revision_number, time, author_ip, renderer) + roll_back_revision = self.revisions[revision_number] + if roll_back_revision.nil? + raise Instiki::ValidationError.new("Revision #{revision_number} not found") + end + author = Author.new(roll_back_revision.author.name, author_ip) + revise(roll_back_revision.content, time, author, renderer) + end + + def revisions? + revisions.size > 1 + end + + def previous_revision(revision) + revision_index = revisions.each_with_index do |rev, index| + if rev.id == revision.id + break index + else + nil + end + end + if revision_index.nil? or revision_index == 0 + nil + else + revisions[revision_index - 1] + end + end + + def references + web.select.pages_that_reference(name) + end + + def wiki_words + wiki_references.select { |ref| ref.wiki_word? }.map { |ref| ref.referenced_name } + end + + def linked_from + web.select.pages_that_link_to(name) + end + + def included_from + web.select.pages_that_include(name) + end + + # Returns the original wiki-word name as separate words, so "MyPage" becomes "My Page". + def plain_name + web.brackets_only? ? name : WikiWords.separate(name) + end + + LOCKING_PERIOD = 30.minutes + + def lock(time, locked_by) + update_attributes(:locked_at => time, :locked_by => locked_by) + end + + def lock_duration(time) + ((time - locked_at) / 60).to_i unless locked_at.nil? + end + + def unlock + update_attribute(:locked_at, nil) + end + + def locked?(comparison_time) + locked_at + LOCKING_PERIOD > comparison_time unless locked_at.nil? + end + + def to_param + name + end + + private + + def continous_revision?(time, author) + (current_revision.author == author) && (revised_at + 30.minutes > time) + end + + # Forward method calls to the current revision, so the page responds to all revision calls + def method_missing(method_id, *args, &block) + method_name = method_id.to_s + # Perform a hand-off to AR::Base#method_missing + if @attributes.include?(method_name) or md = /(=|\?|_before_type_cast)$/.match(method_name) + super(method_id, *args, &block) + else + current_revision.send(method_id) + end + end +end diff --git a/app/models/page_observer.rb b/app/models/page_observer.rb new file mode 100644 index 00000000..aab72fa8 --- /dev/null +++ b/app/models/page_observer.rb @@ -0,0 +1,15 @@ +# This class maintains the state of wiki references for newly created or newly deleted pages +class PageObserver < ActiveRecord::Observer + + def after_create(page) + WikiReference.update_all("link_type = '#{WikiReference::LINKED_PAGE}'", + ['referenced_name = ?', page.name]) + end + + def before_destroy(page) + WikiReference.delete_all ['page_id = ?', page.id] + WikiReference.update_all("link_type = '#{WikiReference::WANTED_PAGE}'", + ['referenced_name = ?', page.name]) + end + +end \ No newline at end of file diff --git a/app/models/page_set.rb b/app/models/page_set.rb new file mode 100644 index 00000000..4ac08c00 --- /dev/null +++ b/app/models/page_set.rb @@ -0,0 +1,92 @@ +# Container for a set of pages with methods for manipulation. + +class PageSet < Array + attr_reader :web + + def initialize(web, pages = nil, condition = nil) + @web = web + # if pages is not specified, make a list of all pages in the web + if pages.nil? + super(web.pages) + # otherwise use specified pages and condition to produce a set of pages + elsif condition.nil? + super(pages) + else + super(pages.select { |page| condition[page] }) + end + end + + def most_recent_revision + self.map { |page| page.revised_at }.max || Time.at(0) + end + + def by_name + PageSet.new(@web, sort_by { |page| page.name }) + end + + alias :sort :by_name + + def by_revision + PageSet.new(@web, sort_by { |page| page.revised_at }).reverse + end + + def pages_that_reference(page_name) + all_referring_pages = WikiReference.pages_that_reference(page_name) + self.select { |page| all_referring_pages.include?(page.name) } + end + + def pages_that_link_to(page_name) + all_linking_pages = WikiReference.pages_that_link_to(page_name) + self.select { |page| all_linking_pages.include?(page.name) } + end + + def pages_that_include(page_name) + all_including_pages = WikiReference.pages_that_include(page_name) + self.select { |page| all_including_pages.include?(page.name) } + end + + def pages_authored_by(author) + all_pages_authored_by_the_author = + Page.connection.select_all(sanitize_sql([ + "SELECT page_id FROM revision WHERE author = '?'", author])) + self.select { |page| page.authors.include?(author) } + end + + def characters + self.inject(0) { |chars,page| chars += page.content.size } + end + + # Returns all the orphaned pages in this page set. That is, + # pages in this set for which there is no reference in the web. + # The HomePage and author pages are always assumed to have + # references and so cannot be orphans + # Pages that refer to themselves and have no links from outside are oprphans. + def orphaned_pages + never_orphans = web.authors + ['HomePage'] + self.select { |page| + if never_orphans.include? page.name + false + else + references = pages_that_reference(page.name) + references.empty? or references == [page] + end + } + end + + # Returns all the wiki words in this page set for which + # there are no pages in this page set's web + def wanted_pages + wiki_words - web.select.names + end + + def names + self.map { |page| page.name } + end + + def wiki_words + self.inject([]) { |wiki_words, page| + wiki_words + page.wiki_words + }.flatten.uniq.sort + end + +end diff --git a/app/models/revision.rb b/app/models/revision.rb new file mode 100644 index 00000000..3af2384d --- /dev/null +++ b/app/models/revision.rb @@ -0,0 +1,4 @@ +class Revision < ActiveRecord::Base + belongs_to :page + composed_of :author, :mapping => [ %w(author name), %w(ip ip) ] +end diff --git a/app/models/system.rb b/app/models/system.rb new file mode 100644 index 00000000..7ac1ad08 --- /dev/null +++ b/app/models/system.rb @@ -0,0 +1,4 @@ +class System < ActiveRecord::Base + set_table_name 'system' + validates_presence_of :password +end \ No newline at end of file diff --git a/app/models/web.rb b/app/models/web.rb new file mode 100644 index 00000000..d1d50268 --- /dev/null +++ b/app/models/web.rb @@ -0,0 +1,147 @@ +class Web < ActiveRecord::Base + has_many :pages + has_many :wiki_files + + def wiki + Wiki.new + end + + def settings_changed?(markup, safe_mode, brackets_only) + self.markup != markup || + self.safe_mode != safe_mode || + self.brackets_only != brackets_only + end + + def add_page(name, content, time, author, renderer) + page = page(name) || Page.new(:web => self, :name => name) + page.revise(content, time, author, renderer) + end + + def authors + connection.select_all( + 'SELECT DISTINCT r.author AS author ' + + 'FROM revisions r ' + + 'JOIN pages p ON p.id = r.page_id ' + + 'WHERE p.web_id = ' + self.id.to_s + + 'ORDER by 1 ' + ).collect { |row| row['author'] } + end + + def categories + select.map { |page| page.categories }.flatten.uniq.sort + end + + def page(name) + pages.find(:first, :conditions => ['name = ?', name]) + end + + def last_page + return Page.find(:first, :order => 'id desc', :conditions => ['web_id = ?', self.id]) + end + + def has_page?(name) + Page.count(['web_id = ? AND name = ?', id, name]) > 0 + end + + def has_file?(file_name) + WikiFile.find_by_file_name(file_name) != nil + end + + def markup + read_attribute('markup').to_sym + end + + def page_names_by_author + connection.select_all( + 'SELECT DISTINCT r.author AS author, p.name AS page_name ' + + 'FROM revisions r ' + + 'JOIN pages p ON r.page_id = p.id ' + + "WHERE p.web_id = #{self.id} " + + 'ORDER by p.name' + ).inject({}) { |result, row| + author, page_name = row['author'], row['page_name'] + result[author] = [] unless result.has_key?(author) + result[author] << page_name + result + } + end + + def remove_pages(pages_to_be_removed) + pages_to_be_removed.each { |p| p.destroy } + end + + def revised_at + select.most_recent_revision + end + + def select(&condition) + PageSet.new(self, pages, condition) + end + + def select_all + PageSet.new(self, pages, nil) + end + + def to_param + address + end + + def create_files_directory + return unless allow_uploads == 1 + dummy_file = self.wiki_files.build(:file_name => '0', :description => '0', :content => '0') + dir = File.dirname(dummy_file.content_path) + begin + require 'fileutils' + FileUtils.mkdir_p dir + dummy_file.save + dummy_file.destroy + rescue => e + logger.error("Failed create files directory for #{self.address}: #{e}") + raise "Instiki could not create directory to store uploaded files. " + + "Please make sure that Instiki is allowed to create directory " + + "#{File.expand_path(dir)} and add files to it." + end + end + + private + + # Returns an array of all the wiki words in any current revision + def wiki_words + pages.inject([]) { |wiki_words, page| wiki_words << page.wiki_words }.flatten.uniq + end + + # Returns an array of all the page names on this web + def page_names + pages.map { |p| p.name } + end + + protected + before_save :sanitize_markup + after_save :create_files_directory + before_validation :validate_address + validates_uniqueness_of :address + validates_length_of :color, :in => 3..6 + + def sanitize_markup + self.markup = markup.to_s + end + + def validate_address + unless address == CGI.escape(address) + self.errors.add(:address, 'should contain only valid URI characters') + raise Instiki::ValidationError.new("#{self.class.human_attribute_name('address')} #{errors.on(:address)}") + end + end + + def default_web? + defined? DEFAULT_WEB and self.address == DEFAULT_WEB + end + + def files_path + if default_web? + "#{RAILS_ROOT}/public/files" + else + "#{RAILS_ROOT}/public/#{self.address}/files" + end + end +end diff --git a/app/models/wiki.rb b/app/models/wiki.rb new file mode 100644 index 00000000..46073065 --- /dev/null +++ b/app/models/wiki.rb @@ -0,0 +1,92 @@ +class Wiki + + cattr_accessor :storage_path, :logger + self.storage_path = "#{RAILS_ROOT}/storage/" + self.logger = RAILS_DEFAULT_LOGGER + + def authenticate(password) + password == (system.password || 'instiki') + end + + def create_web(name, address, password = nil) + @webs = nil + Web.create(:name => name, :address => address, :password => password) + end + + def delete_web(address) + web = Web.find_by_address(address) + unless web.nil? + web.destroy + @webs = nil + end + end + + def edit_web(old_address, new_address, name, markup, color, additional_style, safe_mode = false, + password = nil, published = false, brackets_only = false, count_pages = false, + allow_uploads = true, max_upload_size = nil) + + if not (web = Web.find_by_address(old_address)) + raise Instiki::ValidationError.new("Web with address '#{old_address}' does not exist") + end + + web.update_attributes(:address => new_address, :name => name, :markup => markup, :color => color, + :additional_style => additional_style, :safe_mode => safe_mode, :password => password, :published => published, + :brackets_only => brackets_only, :count_pages => count_pages, :allow_uploads => allow_uploads, :max_upload_size => max_upload_size) + @webs = nil + raise Instiki::ValidationError.new("There is already a web with address '#{new_address}'") unless web.errors.on(:address).nil? + web + end + + def read_page(web_address, page_name) + self.class.logger.debug "Reading page '#{page_name}' from web '#{web_address}'" + web = Web.find_by_address(web_address) + if web.nil? + self.class.logger.debug "Web '#{web_address}' not found" + return nil + else + page = web.pages.find(:first, :conditions => ['name = ?', page_name]) + self.class.logger.debug "Page '#{page_name}' #{page.nil? ? 'not' : ''} found" + return page + end + end + + def remove_orphaned_pages(web_address) + web = Web.find_by_address(web_address) + web.remove_pages(web.select.orphaned_pages) + end + + def revise_page(web_address, page_name, content, revised_at, author, renderer) + page = read_page(web_address, page_name) + page.revise(content, revised_at, author, renderer) + end + + def rollback_page(web_address, page_name, revision_number, time, author_id = nil) + page = read_page(web_address, page_name) + page.rollback(revision_number, time, author_id) + end + + def setup(password, web_name, web_address) + system.update_attribute(:password, password) + create_web(web_name, web_address) + end + + def system + @system ||= (System.find(:first) || System.create) + end + + def setup? + Web.count > 0 + end + + def webs + @webs ||= Web.find(:all).inject({}) { |webs, web| webs.merge(web.address => web) } + end + + def storage_path + self.class.storage_path + end + + def write_page(web_address, page_name, content, written_on, author, renderer) + Web.find_by_address(web_address).add_page(page_name, content, written_on, author, renderer) + end +end \ No newline at end of file diff --git a/app/models/wiki_file.rb b/app/models/wiki_file.rb new file mode 100644 index 00000000..ba122662 --- /dev/null +++ b/app/models/wiki_file.rb @@ -0,0 +1,64 @@ +class WikiFile < ActiveRecord::Base + belongs_to :web + + before_save :write_content_to_file + before_destroy :delete_content_file + + validates_presence_of %w( web file_name ) + validates_length_of :file_name, :within=>1..50 + validates_length_of :description, :maximum=>255 + + def self.find_by_file_name(file_name) + find(:first, :conditions => ['file_name = ?', file_name]) + end + + SANE_FILE_NAME = /^[a-zA-Z0-9\-_\. ]*$/ + def validate + if file_name + if file_name !~ SANE_FILE_NAME + errors.add("file_name", "is invalid. Only latin characters, digits, dots, underscores, " + + "dashes and spaces are accepted") + elsif file_name == '.' or file_name == '..' + errors.add("file_name", "cannot be '.' or '..'") + end + end + + if @web and @content + if (@content.size > @web.max_upload_size.kilobytes) + errors.add("content", "size (#{(@content.size / 1024.0).round} kilobytes) exceeds " + + "the maximum (#{web.max_upload_size} kilobytes) set for this wiki") + end + end + + errors.add("content", "is empty") if @content.nil? or @content.empty? + end + + def content=(content) + if content.respond_to? :read + @content = content.read + else + @content = content + end + end + + def content + @content ||= ( File.open(content_path, 'rb') { |f| f.read } ) + end + + def content_path + web.files_path + '/' + file_name + end + + def write_content_to_file + web.create_files_directory unless File.exists?(web.files_path) + File.open(self.content_path, 'wb') { |f| f.write(@content) } + end + + def delete_content_file + require 'fileutils' + FileUtils.rm_f(content_path) if File.exists?(content_path) + end + + + +end diff --git a/app/models/wiki_reference.rb b/app/models/wiki_reference.rb new file mode 100644 index 00000000..c326e8ad --- /dev/null +++ b/app/models/wiki_reference.rb @@ -0,0 +1,82 @@ +class WikiReference < ActiveRecord::Base + + LINKED_PAGE = 'L' + WANTED_PAGE = 'W' + INCLUDED_PAGE = 'I' + CATEGORY = 'C' + AUTHOR = 'A' + FILE = 'F' + WANTED_FILE = 'E' + + belongs_to :page + validates_inclusion_of :link_type, :in => [LINKED_PAGE, WANTED_PAGE, INCLUDED_PAGE, CATEGORY, AUTHOR, FILE, WANTED_FILE] + + # FIXME all finders below MUST restrict their results to pages belonging to a particular web + + def self.link_type(web, page_name) + web.has_page?(page_name) ? LINKED_PAGE : WANTED_PAGE + end + + def self.pages_that_reference(page_name) + query = 'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' + + 'WHERE wiki_references.referenced_name = ?' + + "AND wiki_references.link_type in ('#{LINKED_PAGE}', '#{WANTED_PAGE}', '#{INCLUDED_PAGE}')" + names = connection.select_all(sanitize_sql([query, page_name])).map { |row| row['name'] } + end + + def self.pages_that_link_to(page_name) + query = 'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' + + 'WHERE wiki_references.referenced_name = ? ' + + "AND wiki_references.link_type in ('#{LINKED_PAGE}', '#{WANTED_PAGE}')" + names = connection.select_all(sanitize_sql([query, page_name])).map { |row| row['name'] } + end + + def self.pages_that_include(page_name) + query = 'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' + + 'WHERE wiki_references.referenced_name = ? ' + + "AND wiki_references.link_type = '#{INCLUDED_PAGE}'" + names = connection.select_all(sanitize_sql([query, page_name])).map { |row| row['name'] } + end + + def self.pages_in_category(category) + query = + 'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' + + 'WHERE wiki_references.referenced_name = ? ' + + "AND wiki_references.link_type = '#{CATEGORY}'" + names = connection.select_all(sanitize_sql([query, category])).map { |row| row['name'] } + end + + def self.list_categories + query = "SELECT DISTINCT referenced_name FROM wiki_references WHERE link_type = '#{CATEGORY}'" + connection.select_all(query).map { |row| row['referenced_name'] } + end + + def wiki_word? + linked_page? or wanted_page? + end + + def wiki_link? + linked_page? or wanted_page? or file? or wanted_file? + end + + def linked_page? + link_type == LINKED_PAGE + end + + def wanted_page? + link_type == WANTED_PAGE + end + + def included_page? + link_type == INCLUDED_PAGE + end + + def file? + link_type == FILE + end + + def wanted_file? + link_type == WANTED_FILE + end + +end diff --git a/app/views/admin/create_system.rhtml b/app/views/admin/create_system.rhtml new file mode 100644 index 00000000..f5d5ff7f --- /dev/null +++ b/app/views/admin/create_system.rhtml @@ -0,0 +1,86 @@ +<% @title = "Instiki Setup"; @content_width = 500 %> + +

    + Congratulations on succesfully installing and starting Instiki. + Since this is the first time Instiki has been run on this port, + you'll need to do a brief one-time setup. +

    + +<%= form_tag({ :controller => 'admin', :action => 'create_system' }, + { 'id' => 'setup', 'method' => 'post', 'onSubmit' => 'return validateSetup()', + 'accept-charset' => 'utf-8' }) +%> +
      +
    1. + +

      Name and address for your first web

      +
      + The name of the web is included in the title on all pages. + The address is the base path that all pages within the web live beneath. + Ex: the address "rails" gives URLs like /rails/show/HomePage. + The address can only consist of letters and digits. +
      +
      + Name: +    + Address: +
      +
    2. + +
    3. +

      Password for creating and changing webs

      +
      + Administrative access allows you to make new webs and change existing ones. +
      +
      Everyone with this password will be able to do this, so pick it carefully!
      +
      + Password: +    + Verify: +
      +
    4. +
    + +

    + +

    +<%= end_form_tag %> + + diff --git a/app/views/admin/create_web.rhtml b/app/views/admin/create_web.rhtml new file mode 100644 index 00000000..5b9e3f7e --- /dev/null +++ b/app/views/admin/create_web.rhtml @@ -0,0 +1,72 @@ +<% @title = "New Wiki Web"; @content_width = 500 %> + +

    + Each web serves as an isolated name space for wiki pages, + so different subjects or projects can write about different MuppetShows. +

    + +<%= form_tag({ :controller => 'admin', :action => 'create_web' }, + { 'id' => 'setup', 'method' => 'post', + 'onSubmit' => 'cleanAddress(); return validateSetup()', + 'accept-charset' => 'utf-8' }) +%> + +
      +
    1. +

      Name and address for your new web

      +
      + The name of the web is included in the title on all pages. + The address is the base path that all pages within the web live beneath. + Ex: the address "rails" gives URLs like /rails/show/HomePage. + The address can only consist of letters and digits. +
      +
      + Name: +    + Address: +
      +
    2. +
    + + +

    + + Enter system password + + and + + +

    + +<%= end_form_tag %> + + diff --git a/app/views/admin/edit_web.rhtml b/app/views/admin/edit_web.rhtml new file mode 100644 index 00000000..3062892f --- /dev/null +++ b/app/views/admin/edit_web.rhtml @@ -0,0 +1,136 @@ +<% @title = "Edit Web" %> + +<%= form_tag({ :controller => 'admin', :action => 'edit_web', :web => @web.address }, + { 'id' => 'setup', 'method' => 'post', + 'onSubmit' => 'cleanAddress(); return validateSetup()', + 'accept-charset' => 'utf-8' }) +%> + +

    Name and address

    +
    + The name of the web is included in the title on all pages. + The address is the base path that all pages within the web live beneath. + Ex: the address "rails" gives URLs like /rails/show/HomePage. +
    + +
    + Name:    + Address: + (Letters and digits only) +
    + +

    Specialize

    +
    + Markup: + + +    + + Color: + +
    +

    + + /> + Safe mode + - strip HTML tags and stylesheet options from the content of all pages +
    + /> + Brackets only + - require all wiki words to be as [[wiki word]], WikiWord links won't be created +
    + /> + Count pages +
    + + /> + Allow uploads of no more than + + kbytes + - + allow users to upload pictures and other files and include them on wiki pages + +
    +
    +

    + + + Stylesheet tweaks >> + + - add or change styles used by this web; styles defined here take precedence over + instiki.css. Hint: View HTML source of a page you want to style to find ID names on individual + tags. +
    + +
    + +

    Password protection for this web (<%= @web.name %>)

    +
    + This is the password that visitors need to view and edit this web. + Setting the password to nothing will remove the password protection. +
    +
    + Password: +    + Verify: +
    + +

    Publish read-only version of this web (<%= @web.name %>)

    +
    + You can turn on a read-only version of this web that's accessible even when the regular web + is password protected. + The published version is accessible through URLs like /wiki/published/HomePage. +
    +
    + /> + Publish this web +
    + +

    + + Enter system password + + and + +

    + ...or forget changes and <%= link_to 'create a new web', :action => 'create_web' %> +
    +

    + +<%= end_form_tag %> + +
    +

    Other administrative tasks

    + +<%= form_tag({:controller => 'admin', :web => @web.address, :action => 'remove_orphaned_pages'}, + { :id => 'remove_orphaned_pages', + :onSubmit => "return checkSystemPassword(document.getElementById('system_password_orphaned').value)", + 'accept-charset' => 'utf-8' }) +%> +

    + + Clean up by entering system password + + and + + +

    +<%= end_form_tag %> + +<%= javascript_include_tag 'edit_web' %> diff --git a/app/views/file/file.rhtml b/app/views/file/file.rhtml new file mode 100644 index 00000000..9f67b9b8 --- /dev/null +++ b/app/views/file/file.rhtml @@ -0,0 +1,33 @@ +<% + @title = "Upload #{h @file_name}" + @hide_navigation = false +%> + +<%= error_messages_for 'file' %> + +<%= form_tag({ :controller => 'file', :web => @web_name, :action => 'file' }, + { 'multipart' => true , 'accept-charset' => 'utf-8' }) %> + <%= hidden_field 'file', 'file_name' %> +
    + Content of <%= h @file_name %> to upload (required): +
    + +
    + + Please note that the file you are uploadng will be named <%= h @file_name %> on the wiki - + regardless of how it is named on your computer. To change the wiki name of the file, please go + <%= link_to :back %> and edit the wiki page that refers to the file. + +
    +
    + Description (optional): +
    + <%= text_field "file", "description", "size" => 40 %> +
    +
    + as + <%= text_field_tag :author, @author, + :onfocus => "this.value == 'AnonymousCoward' ? this.value = '' : true;", + :onblur => "this.value == '' ? this.value = 'AnonymousCoward' : true" %> +
    +<%= end_form_tag %> \ No newline at end of file diff --git a/app/views/file/import.rhtml b/app/views/file/import.rhtml new file mode 100644 index 00000000..910ccef4 --- /dev/null +++ b/app/views/file/import.rhtml @@ -0,0 +1,23 @@ +

    +<%= form_tag({}, { 'multipart' => true, 'accept-charset' => 'utf-8' }) %> +

    + File to upload: +
    + +

    +

    + System password: +
    + +

    +

    + as + + <% if @page %> + | <%= link_to 'Cancel', :web => @web.address, :action => 'file'%> (unlocks page) + <% end %> + +

    +<%= end_form_tag %> +

    \ No newline at end of file diff --git a/app/views/layouts/default.rhtml b/app/views/layouts/default.rhtml new file mode 100644 index 00000000..67b85e06 --- /dev/null +++ b/app/views/layouts/default.rhtml @@ -0,0 +1,79 @@ + + + + + <% if @page and (@page.name == 'HomePage') and (%w( show published print ).include?(@action_name)) %> + <%= h @web.name %> + <% elsif @web %> + <%= @title %> in <%= h @web.name %> + <% else %> + <%= @title %> + <% end %> + <%= @show_diff ? ' (changes)' : '' %> + + + + + + + + <%= stylesheet_link_tag 'instiki' unless @inline_style %> + + + + <% if @web %> + <%= auto_discovery_link_tag(:rss, :controller => 'wiki', :web => @web.address, :action => 'rss_with_headlines') %> + <%= auto_discovery_link_tag(:rss, :controller => 'wiki', :web => @web.address, :action => 'rss_with_content') %> + <% end %> + + + + +
    +
    +

    + <% if @page and (@page.name == 'HomePage') and %w( show published print ).include?(@action_name) %> + <%= h(@web.name) + (@show_diff ? ' (changes)' : '') %> + <% elsif @web %> + <%= @web.name %>
    + <%= @title %> + <% else %> + <%= @title %> + <% end %> +

    + +<%= render 'navigation' unless @web.nil? || @hide_navigation %> + +<% if @flash[:info] %> +
    <%= escape_preserving_linefeeds @flash[:info] %>
    +<% end %> + +<% if @error or @flash[:error] %> +
    <%= escape_preserving_linefeeds(@error || @flash[:error]) %>
    +<% end %> + +<%= @content_for_layout %> + +<% if @show_footer %> + +<% end %> + +
    + +
    + + + diff --git a/app/views/markdown_help.rhtml b/app/views/markdown_help.rhtml new file mode 100644 index 00000000..067be08d --- /dev/null +++ b/app/views/markdown_help.rhtml @@ -0,0 +1,12 @@ +

    Markdown formatting tips (advanced)

    + + + + + + + + + + +
    _your text_your text
    **your text**your text
    `my code`my code
    * Bulleted list
    * Second item
    • Bulleted list
    • Second item
    1. Numbered list
    1. Second item
    1. Numbered list
    2. Second item
    [link name](URL)link name
    ***Horizontal ruler
    <http://url>
    <email@add.com>
    Auto-linked
    ![Alt text](URL)Image
    diff --git a/app/views/mixed_help.rhtml b/app/views/mixed_help.rhtml new file mode 100644 index 00000000..58503f54 --- /dev/null +++ b/app/views/mixed_help.rhtml @@ -0,0 +1,7 @@ +<%= render 'textile_help' %> + +

    Markdown

    +

    + In addition to Textile, this wiki also understands + Markdown. +

    \ No newline at end of file diff --git a/app/views/navigation.rhtml b/app/views/navigation.rhtml new file mode 100644 index 00000000..5735b472 --- /dev/null +++ b/app/views/navigation.rhtml @@ -0,0 +1,28 @@ +<% +def list_item(text, link_options, description, accesskey = nil) + link_options[:controller] = 'wiki' + link_options[:web] = @web.address + link_to_unless_current(text, link_options, :title => description, :accesskey => accesskey) { + content_tag('b', text, 'title' => description, 'class' => 'navOn') + } +end +%> + + \ No newline at end of file diff --git a/app/views/rdoc_help.rhtml b/app/views/rdoc_help.rhtml new file mode 100644 index 00000000..1afaff5c --- /dev/null +++ b/app/views/rdoc_help.rhtml @@ -0,0 +1,12 @@ +

    RDoc formatting tips (advanced)

    + + + + + + + + + + +
    _your text_your text
    *your text*your text
    * Bulleted list
    * Second item
    • Bulleted list
    • Second item
    1. Numbered list
    2. Second item
    1. Numbered list
    2. Second item
    +my_code+my_code
    ---Horizontal ruler
    [[URL linkname]]linkname
    http://url
    mailto:e@add.com
    Auto-linked
    imageURLImage
    diff --git a/app/views/textile_help.rhtml b/app/views/textile_help.rhtml new file mode 100644 index 00000000..3d8400b3 --- /dev/null +++ b/app/views/textile_help.rhtml @@ -0,0 +1,24 @@ +

    Textile formatting tips (advanced)

    + + + + + + + + + + +
    _your text_your text
    *your text*your text
    %{color:red}hello%hello
    * Bulleted list
    * Second item
    • Bulleted list
    • Second item
    # Numbered list
    # Second item
    1. Numbered list
    2. Second item
    "linkname":URLlinkname
    |a|table|row|
    |b|table|row|
    Table
    http://url
    email@address.com
    Auto-linked
    !imageURL!Image
    + + diff --git a/app/views/wiki/_inbound_links.rhtml b/app/views/wiki/_inbound_links.rhtml new file mode 100644 index 00000000..4c5e4e12 --- /dev/null +++ b/app/views/wiki/_inbound_links.rhtml @@ -0,0 +1,13 @@ +<% unless @page.linked_from.empty? %> + + | Linked from: + <%= @page.linked_from.collect { |referring_page| link_to_existing_page referring_page }.join(", ") %> + +<% end %> + +<% unless @page.included_from.empty? %> + + | Included from: + <%= @page.included_from.collect { |referring_page| link_to_existing_page referring_page }.join(", ") %> + +<% end %> diff --git a/app/views/wiki/authors.rhtml b/app/views/wiki/authors.rhtml new file mode 100644 index 00000000..57f529ee --- /dev/null +++ b/app/views/wiki/authors.rhtml @@ -0,0 +1,11 @@ +<% @title = 'Authors' %> + +
      + <% for author in @authors %> +
    • + <%= link_to_page author %> + co- or authored: + <%= @page_names_by_author[author].collect { |page_name| link_to_page(page_name) }.sort.join ', ' %> +
    • + <% end %> +
    diff --git a/app/views/wiki/edit.rhtml b/app/views/wiki/edit.rhtml new file mode 100644 index 00000000..ad7df15d --- /dev/null +++ b/app/views/wiki/edit.rhtml @@ -0,0 +1,40 @@ +<% + @title = "Editing #{@page.name}" + @content_width = 720 + @hide_navigation = true +%> + +
    + <%= render("#{@web.markup}_help") %> + <%= render 'wiki_words_help' %> +
    + +
    + <%= form_tag({ :action => 'save', :web => @web.address, :id => @page.name }, + { 'id' => 'editForm', 'method' => 'post', 'onSubmit' => 'cleanAuthorName()', + 'accept-charset' => 'utf-8' }) %> + + +
    + as + <%= text_field_tag :author, @author, + :onfocus => "this.value == 'AnonymousCoward' ? this.value = '' : true;", + :onblur => "this.value == '' ? this.value = 'AnonymousCoward' : true" %> + | + + <%= link_to('Cancel', {:web => @web.address, :action => 'cancel_edit', :id => @page.name}, + {:accesskey => 'c'}) %> + (unlocks page) + +
    + <%= end_form_tag %> +
    + + diff --git a/app/views/wiki/export.rhtml b/app/views/wiki/export.rhtml new file mode 100644 index 00000000..d8bc3f02 --- /dev/null +++ b/app/views/wiki/export.rhtml @@ -0,0 +1,12 @@ +<% @title = "Export" %> + +

    You can export all the pages in this web as a zip file in either HTML (with working links and all) or the pure markup (to import in another wiki).

    + +
      +
    • <%= link_to 'HTML', :web => @web.address, :action => 'export_html' %>
    • +
    • <%= link_to "Markup (#{@web.markup.to_s.capitalize})", :web => @web.address, :action => 'export_markup' %>
    • +<% if OPTIONS[:pdflatex] && @web.markup == :textile %> +
    • <%= link_to 'TeX', :web => @web.address, :action => 'export_tex' %>
    • +
    • <%= link_to 'PDF', :web => @web.address, :action => 'export_pdf' %>
    • +<% end %> +
    diff --git a/app/views/wiki/feeds.rhtml b/app/views/wiki/feeds.rhtml new file mode 100644 index 00000000..389d6983 --- /dev/null +++ b/app/views/wiki/feeds.rhtml @@ -0,0 +1,14 @@ +<% @title = "Feeds" %> + +

    You can subscribe to this wiki by RSS and get either just the headlines of the pages that change or the entire page.

    + +
      +
    • + <% if @rss_with_content_allowed %> + <%= link_to 'Full content (RSS 2.0)', :web => @web.address, :action => :rss_with_content %> + <% end %> +
    • +
    • + <%= link_to 'Headlines (RSS 2.0)', :web => @web.address, :action => :rss_with_headlines %> +
    • +
    diff --git a/app/views/wiki/list.rhtml b/app/views/wiki/list.rhtml new file mode 100644 index 00000000..e1b584be --- /dev/null +++ b/app/views/wiki/list.rhtml @@ -0,0 +1,64 @@ +<% @title = "All Pages" %> + +<%= categories_menu unless @categories.empty? %> + +
    +<% unless @pages_that_are_orphaned.empty? && @page_names_that_are_wanted.empty? %> +

    + All Pages +
    All pages in <%= @set_name %> listed alphabetically +

    +<% end %> + +
      + <% @pages_in_category.each do |page| %> +
    • + <%= link_to_existing_page page, truncate(page.plain_name, 35) %> +
    • +<% end %>
    + +<% if @web.count_pages? %> + <% total_chars = @pages_in_category.characters %> +

    All content: <%= total_chars %> chars / approx. <%= sprintf("%-.1f", (total_chars / 2275 )) %> printed pages

    +<% end %> +
    + +
    +<% unless @page_names_that_are_wanted.empty? %> +

    + Wanted Pages +
    + + Unexisting pages that other pages in <%= @set_name %> reference + +

    + +
      + <% @page_names_that_are_wanted.each do |wanted_page_name| %> +
    • + <%= link_to_page(wanted_page_name, @web, truncate(WikiWords.separate(wanted_page_name), 35)) %> + wanted by + <%= @web.select.pages_that_reference(wanted_page_name).collect { |referring_page| + link_to_existing_page referring_page + }.join(", ") + %> +
    • + <% end %> +
    +<% end %> + +<% unless @pages_that_are_orphaned.empty? %> +

    + Orphaned Pages +
    Pages in <%= @set_name %> that no other page reference +

    + +
      + <% @pages_that_are_orphaned.each do |orphan_page| %> +
    • + <%= link_to_existing_page orphan_page, truncate(orphan_page.plain_name, 35) %> +
    • + <% end %> +
    +<% end %> +
    diff --git a/app/views/wiki/locked.rhtml b/app/views/wiki/locked.rhtml new file mode 100644 index 00000000..ee0cf814 --- /dev/null +++ b/app/views/wiki/locked.rhtml @@ -0,0 +1,23 @@ +<% @title = "#{@page.plain_name} is locked" %> + +

    + <%= link_to_page(@page.locked_by) %> + <% if @page.lock_duration(Time.now) == 0 %> + just started editing this page. + <% else %> + has been editing this page for <%= @page.lock_duration(Time.now) %> minutes. + <% end %> +

    + +

    + <%= link_to 'Edit the page anyway', + {:web => @web_name, :action => 'edit', :id => @page.name, :params => {'break_lock' => '1'} }, + {:accesskey => 'E'} + %> + + <%= link_to 'Cancel', + {:web => @web_name, :action => 'show', :id => @page.name}, + {:accesskey => 'C'} + %> + +

    diff --git a/app/views/wiki/login.rhtml b/app/views/wiki/login.rhtml new file mode 100644 index 00000000..15582524 --- /dev/null +++ b/app/views/wiki/login.rhtml @@ -0,0 +1,22 @@ +<% @title = "#{@web_name} Login" %><% @hide_navigation = true %> + +

    +<%= form_tag({ :controller => 'wiki', :action => 'authenticate', :web => @web.address}, + { 'name' => 'loginForm', 'id' => 'loginForm', 'method' => 'post', 'accept-charset' => 'utf-8' }) %> +

    + This web is password-protected. Please enter the password. + <% if @web.published? %> + If you don't have the password, you can view this wiki as a <%= link_to 'read-only version', :action => 'published', :id => 'HomePage' %>. + <% end %> +

    +

    + Password: + + +

    +<%= end_form_tag %> +

    + + diff --git a/app/views/wiki/new.rhtml b/app/views/wiki/new.rhtml new file mode 100644 index 00000000..cda26081 --- /dev/null +++ b/app/views/wiki/new.rhtml @@ -0,0 +1,33 @@ +<% + @title = "Creating #{WikiWords.separate(@page_name)}" + @content_width = 720 + @hide_navigation = true +%> + +
    + <%= render("#{@web.markup}_help") %> + <%= render 'wiki_words_help' %> +
    + +
    + <%= form_tag({ :action => 'save', :web => @web.address, :id => @page_name }, + { 'id' => 'editForm', 'method' => 'post', 'onSubmit' => 'cleanAuthorName();', 'accept-charset' => 'utf-8' }) %> + + +
    + as + <%= text_field_tag :author, @author, + :onfocus => "this.value == 'AnonymousCoward' ? this.value = '' : true;", + :onblur => "this.value == '' ? this.value = 'AnonymousCoward' : true" %> +
    + <%= end_form_tag %> +
    + + diff --git a/app/views/wiki/page.rhtml b/app/views/wiki/page.rhtml new file mode 100644 index 00000000..725f1184 --- /dev/null +++ b/app/views/wiki/page.rhtml @@ -0,0 +1,51 @@ +<% + @title = @page.plain_name + @title += ' (changes)' if @show_diff + @show_footer = true +%> + +
    + <% if @show_diff %> +

    + + Showing changes from revision #<%= @page.revisions.size - 1 %> to #<%= @page.revisions.size %>: + Added | Removed + +

    + <%= @renderer.display_diff %> + <% else %> + <%= @renderer.display_content %> + <% end %> +
    + + + + diff --git a/app/views/wiki/print.rhtml b/app/views/wiki/print.rhtml new file mode 100644 index 00000000..177e92f1 --- /dev/null +++ b/app/views/wiki/print.rhtml @@ -0,0 +1,14 @@ +<% + @title = @page.plain_name + @hide_navigation = true + @style_additions = ".newWikiWord { background-color: white; font-style: italic; }" + @inline_style = true +%> + +<%= @renderer.display_content_for_export %> + + diff --git a/app/views/wiki/published.rhtml b/app/views/wiki/published.rhtml new file mode 100644 index 00000000..b394583c --- /dev/null +++ b/app/views/wiki/published.rhtml @@ -0,0 +1,9 @@ +<% + @title = @page.plain_name + @hide_navigation = false + @style_additions = ".newWikiWord { background-color: white; font-style: italic; }" + @inline_style = true + @show_footer = true +%> + +<%= @renderer.display_published %> diff --git a/app/views/wiki/recently_revised.rhtml b/app/views/wiki/recently_revised.rhtml new file mode 100644 index 00000000..f4411fba --- /dev/null +++ b/app/views/wiki/recently_revised.rhtml @@ -0,0 +1,19 @@ +<% @title = "Recently Revised" %> + +<%= categories_menu %> + +<% @pages_by_day.keys.sort.reverse.each do |day| %> +

    <%= format_date(day, include_time = false) %>

    +
      + <% for page in @pages_by_day[day] %> +
    • + <%= link_to_existing_page page %> + +
    • + <% end %> +
    +<% end %> diff --git a/app/views/wiki/revision.rhtml b/app/views/wiki/revision.rhtml new file mode 100644 index 00000000..2c0bcefe --- /dev/null +++ b/app/views/wiki/revision.rhtml @@ -0,0 +1,28 @@ +<% + @title = "#{@page.plain_name} (Rev ##{@revision_number}#{@show_diff ? ', changes' : ''})" +%> + + +
    + <% if @show_diff %> +

    + + Showing changes from revision #<%= @revision_number - 1 %> to #<%= @revision_number %>: + Added | Removed + +

    + <%= @renderer.display_diff %> + <% else %> + <%= @renderer.display_content %> + <% end %> +
    + + + + diff --git a/app/views/wiki/rollback.rhtml b/app/views/wiki/rollback.rhtml new file mode 100644 index 00000000..0e4cbea2 --- /dev/null +++ b/app/views/wiki/rollback.rhtml @@ -0,0 +1,39 @@ +<% + @title = "Rollback to #{@page.plain_name} Rev ##{@revision_number}" + @content_width = 720 + @hide_navigation = true +%> + +<%= "

    Please correct the error that caused this error in rendering:
    #{@params["msg"]}

    " if @params["msg"] %> + +
    + <%= render("#{@web.markup}_help") %> + <%= render 'wiki_words_help' %> +
    + +
    + <%= form_tag({:web => @web.address, :action => 'save', :id => @page.name}, + { :id => 'editForm', :method => 'post', :onSubmit => 'cleanAuthorName();', + 'accept-charset' => 'utf-8' }) %> + +
    + as + + | + + <%= link_to('Cancel', {:web => @web.address, :action => 'cancel_edit', :id => @page.name}, + {:accesskey => 'c'}) %> + (unlocks page) + +
    + <%= end_form_tag %> +
    + + diff --git a/app/views/wiki/rss_feed.rxml b/app/views/wiki/rss_feed.rxml new file mode 100644 index 00000000..84e29dbe --- /dev/null +++ b/app/views/wiki/rss_feed.rxml @@ -0,0 +1,21 @@ +xml.rss('version' => '2.0') do + xml.channel do + xml.title(@web.name) + xml.link(url_for(:only_path => false, :web => @web_name, :action => @link_action, :id => 'HomePage')) + xml.description('An Instiki wiki') + xml.language('en-us') + xml.ttl('40') + + for page in @pages_by_revision + xml.item do + xml.title(page.plain_name) + unless @hide_description + xml.description(rendered_content(page)) + end + xml.pubDate(page.revised_at.getgm.strftime('%a, %d %b %Y %H:%M:%S Z')) + xml.guid(url_for(:only_path => false, :web => @web_name, :action => @link_action, :id => page.name)) + xml.link(url_for(:only_path => false, :web => @web_name, :action => @link_action, :id => page.name)) + end + end + end +end diff --git a/app/views/wiki/search.rhtml b/app/views/wiki/search.rhtml new file mode 100644 index 00000000..789f867c --- /dev/null +++ b/app/views/wiki/search.rhtml @@ -0,0 +1,38 @@ +<% @title = "Search results for \"#{h @params["query"]}\"" %> + +<% unless @title_results.empty? %> +

    <%= @title_results.length %> page(s) containing search string in the page name:

    +
      + <% for page in @title_results %> +
    • + <%= link_to page.plain_name, :web => @web.address, :action => 'show', :id => page.name %> +
    • + <% end %> +
    +<% end %> + + +<% unless @results.empty? %> +

    <%= @results.length %> page(s) containing search string in the page text:

    +
      + <% for page in @results %> +
    • + <%= link_to page.plain_name, :web => @web.address, :action => 'show', :id => page.name %> +
    • + <% end %> +
    +<% end %> + +<% if (@results + @title_results).empty? %> +

    No pages contain "<%= h @params["query"] %>"

    +

    + Perhaps you should try expanding your query. Remember that Instiki searches for entire + phrases, so if you search for "all that jazz" it will not match pages that contain these + words in separation—only as a sentence fragment. +

    +

    + If you're a high-tech computer wizard, you might even want try constructing a Ruby regular + expression. That's actually what Instiki uses, so go right ahead and flex your + "[a-z]*Leet?RegExpSkill(s|z)" +

    +<% end %> diff --git a/app/views/wiki/tex.rhtml b/app/views/wiki/tex.rhtml new file mode 100644 index 00000000..ea9a06c6 --- /dev/null +++ b/app/views/wiki/tex.rhtml @@ -0,0 +1,23 @@ +\documentclass[12pt,titlepage]{article} + +\usepackage[danish]{babel} %danske tekster +\usepackage[OT1]{fontenc} %rigtige danske bogstaver... +\usepackage{a4} +\usepackage{graphicx} +\usepackage{ucs} +\usepackage[utf8x]{inputenc} +\input epsf + +%------------------------------------------------------------------- + +\begin{document} + +\sloppy + +%------------------------------------------------------------------- + +\section*{<%= @page.name %>} + +<%= @tex_content %> + +\end{document} \ No newline at end of file diff --git a/app/views/wiki/tex_web.rhtml b/app/views/wiki/tex_web.rhtml new file mode 100644 index 00000000..45953c52 --- /dev/null +++ b/app/views/wiki/tex_web.rhtml @@ -0,0 +1,35 @@ +\documentclass[12pt,titlepage]{article} + +\usepackage{fancyhdr} +\pagestyle{fancy} + +\fancyhead[LE,RO]{} +\fancyhead[LO,RE]{\nouppercase{\bfseries \leftmark}} +\fancyfoot[C]{\thepage} + +\usepackage[danish]{babel} %danske tekster +\usepackage{a4} +\usepackage{graphicx} +\usepackage{ucs} +\usepackage[utf8]{inputenc} +\input epsf + + +%------------------------------------------------------------------- + +\title{<%= @web_name %>} + +\begin{document} + +\maketitle + +\tableofcontents +\pagebreak + +\sloppy + +%------------------------------------------------------------------- + +<%= @tex_content %> + +\end{document} \ No newline at end of file diff --git a/app/views/wiki/web_list.rhtml b/app/views/wiki/web_list.rhtml new file mode 100644 index 00000000..4f9608b0 --- /dev/null +++ b/app/views/wiki/web_list.rhtml @@ -0,0 +1,25 @@ +<% @title = "Wiki webs" %> +
    + +<% @webs.each do |web| %> + + <% if web.password %>
    + <% else %>
    <% end %> + + <%= link_to_page 'HomePage', web, web.name, :mode => 'show' %> + <% if web.published? %> + (<%= link_to_page 'HomePage', web, 'published version', :mode => 'publish' %>) + <% end %> + + + +

    +<% end %> + diff --git a/app/views/wiki_words_help.rhtml b/app/views/wiki_words_help.rhtml new file mode 100644 index 00000000..b283b407 --- /dev/null +++ b/app/views/wiki_words_help.rhtml @@ -0,0 +1,9 @@ +

    Wiki words

    +

    + Two or more uppercase words stuck together (camel case) or any phrase surrounded by double + brackets is a wiki word. A camel-case wiki word can be escaped by putting \ in front of it. +

    +

    + Wiki words: HomePage, ThreeWordsTogether, [[C++]], [[Let's play again!]]
    + Not wiki words: IBM, School +

    diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 00000000..b829644d --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,17 @@ +unless defined?(RAILS_ROOT) + root_path = File.join(File.dirname(__FILE__), '..') + unless RUBY_PLATFORM =~ /mswin32/ + require 'pathname' + root_path = Pathname.new(root_path).cleanpath.to_s + end + RAILS_ROOT = root_path +end + +if File.directory?("#{RAILS_ROOT}/vendor/rails") + require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" +else + require 'rubygems' + require 'initializer' +end + +Rails::Initializer.run(:set_load_path) \ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 00000000..015390e1 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,105 @@ + +# "Out of the box", Instiki stores it's data in sqlite3 database. Other options are listed below. + +development: + adapter: sqlite3 + database: db/development.db.sqlite3 + +test: + adapter: sqlite3 + database: db/test.db.sqlite3 + +production: + adapter: sqlite3 + database: db/production.db.sqlite3 + +# MySQL (default setup). Versions 4.1 and 5.0 are recommended. +# +# Install the MySQL driver: +# gem install mysql +# On MacOS X: +# gem install mysql -- --include=/usr/local/lib +# On Windows: +# There is no gem for Windows. Install mysql.so from RubyForApache. +# http://rubyforge.org/projects/rubyforapache +# +# And be sure to use new-style password hashing: +# http://dev.mysql.com/doc/refman/5.0/en/old-client.html + + +# Get the fast C bindings: +# gem install mysql +# (on OS X: gem install mysql -- --include=/usr/local/lib) +# And be sure to use new-style password hashing: +# http://dev.mysql.com/doc/refman/5.0/en/old-client.html + +mysql_example: + adapter: mysql + database: instiki_development + username: root + password: + socket: /path/to/your/mysql.sock + +# Connect on a TCP socket. If omitted, the adapter will connect on the +# domain socket given by socket instead. +#host: localhost +#port: 3306 + +# Warning: The database defined as 'test' will be erased and +# re-generated from your development database when you run 'rake'. +# Do not set this db to the same as development or production. +mysql_example: + adapter: mysql + database: instiki_test + username: root + password: + socket: /path/to/your/mysql.sock + +# PostgreSQL versions 7.4 - 8.2 +# +# Get the C bindings: +# gem install postgres +# or use the pure-Ruby bindings (the only know way on Windows): +# gem install postgres-pr + +postgresql_example: + adapter: postgresql + database: instiki_development + username: instiki + password: + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. + #host: remote-database + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Character set encoding. The server defaults to sql_ascii. + #encoding: UTF8 + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # info, notice, warning, error, log, fatal, or panic + # The server defaults to notice. + #min_messages: warning + + +# SQLite version 2.x +# gem install sqlite-ruby +sqlite_example: + adapter: sqlite + database: db/development.sqlite2 + + +# SQLite version 3.x +# gem install sqlite3-ruby +sqlite3_example: + adapter: sqlite3 + database: db/development.sqlite3 + +# In-memory SQLite 3 database. Useful for tests. +sqlite3_in_memory_example: + adapter: sqlite3 + database: ":memory:" diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 00000000..24e89a88 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,29 @@ +# Bootstrap the Rails environment, frameworks, and default configuration +require File.join(File.dirname(__FILE__), 'boot') + +Rails::Initializer.run do |config| + # Skip frameworks you're not going to use + config.frameworks -= [ :action_web_service, :action_mailer ] + + # Use the database for sessions instead of the file system + # (create the session table with 'rake create_sessions_table') + config.action_controller.session_store = :active_record_store + + # Enable page/fragment caching by setting a file-based store + # (remember to create the caching directory and make it readable to the application) + #config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache" + + # Activate observers that should always be running + config.active_record.observers = :page_observer + + # Use Active Record's schema dumper instead of SQL when creating the test database + # (enables use of different database adapters for development and test environments) + config.active_record.schema_format = :ruby + + config.load_paths << "#{RAILS_ROOT}/vendor/plugins/sqlite3-ruby" +end + +# Instiki-specific configuration below +require_dependency 'instiki_errors' + +require 'jcode' diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 00000000..a151c3ba --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,17 @@ +# In the development environment your application's code is reloaded on +# every request. This slows down response time but is perfect for development +# since you don't have to restart the webserver when you make code changes. +config.cache_classes = false + +# Log error messages when you accidentally call methods on nil. +config.whiny_nils = true + +# Enable the breakpoint server that script/breakpointer connects to +config.breakpoint_server = true + +# Show full error reports and disable caching +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false + +# Don't care if the mailer can't send +config.action_mailer.raise_delivery_errors = false diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 00000000..f2b9ed68 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,17 @@ +# The production environment is meant for finished, "live" apps. +# Code is not reloaded between requests +config.cache_classes = true + +# Use a different logger for distributed setups +# config.logger = SyslogLogger.new + + +# Full error reports are disabled and caching is turned on +config.action_controller.consider_all_requests_local = false +config.action_controller.perform_caching = true + +# Enable serving of images, stylesheets, and javascripts from an asset server +# config.action_controller.asset_host = "http://assets.example.com" + +# Disable delivery errors if you bad email addresses should just be ignored +# config.action_mailer.raise_delivery_errors = false diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 00000000..66b823dc --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,23 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! +config.cache_classes = true + +# Log error messages when you accidentally call methods on nil. +config.whiny_nils = true + +# Show full error reports and disable caching +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false + +# Tell ActionMailer not to deliver emails to the real world. +# The :test delivery method accumulates sent emails in the +# ActionMailer::Base.deliveries array. +config.action_mailer.delivery_method = :test + +# Overwrite the default settings for fixtures in tests. See Fixtures +# for more details about these settings. +# config.transactional_fixtures = true +# config.instantiated_fixtures = false +# config.pre_loaded_fixtures = false \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 00000000..a5d49bab --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,38 @@ +# Create a route to DEFAULT_WEB, if such is specified; also register a generic route +def connect_to_web(map, generic_path, generic_routing_options) + if defined? DEFAULT_WEB + explicit_path = generic_path.gsub(/:web\/?/, '') + explicit_routing_options = generic_routing_options.merge(:web => DEFAULT_WEB) + map.connect(explicit_path, explicit_routing_options) + end + map.connect(generic_path, generic_routing_options) +end + +ActionController::Routing::Routes.draw do |map| + map.connect 'create_system', :controller => 'admin', :action => 'create_system' + map.connect 'create_web', :controller => 'admin', :action => 'create_web' + map.connect 'remove_orphaned_pages', :controller => 'admin', :action => 'remove_orphaned_pages' + map.connect 'delete_web', :controller => 'admin', :action => 'delete_web' + map.connect 'web_list', :controller => 'wiki', :action => 'web_list' + + connect_to_web map, ':web/edit_web', :controller => 'admin', :action => 'edit_web' + connect_to_web map, ':web/files/:id', :controller => 'file', :action => 'file' + connect_to_web map, ':web/import/:id', :controller => 'file', :action => 'import' + connect_to_web map, ':web/login', :controller => 'wiki', :action => 'login' + connect_to_web map, ':web/web_list', :controller => 'wiki', :action => 'web_list' + connect_to_web map, ':web/show/diff/:id', :controller => 'wiki', :action => 'show', :mode => 'diff' + connect_to_web map, ':web/revision/diff/:id', :controller => 'wiki', :action => 'revision', :mode => 'diff' + connect_to_web map, ':web/list', :controller => 'wiki', :action => 'list' + connect_to_web map, ':web/list/:category', :controller => 'wiki', :action => 'list' + connect_to_web map, ':web/recently_revised', :controller => 'wiki', :action => 'recently_revised' + connect_to_web map, ':web/recently_revised/:category', :controller => 'wiki', :action => 'recently_revised' + connect_to_web map, ':web/:action/:id', :controller => 'wiki' + connect_to_web map, ':web/:action', :controller => 'wiki' + connect_to_web map, ':web', :controller => 'wiki', :action => 'index' + + if defined? DEFAULT_WEB + map.connect '', :controller => 'wiki', :web => DEFAULT_WEB, :action => 'index' + else + map.connect '', :controller => 'wiki', :action => 'index' + end +end diff --git a/config/spam_patterns.txt b/config/spam_patterns.txt new file mode 100644 index 00000000..5c12addc --- /dev/null +++ b/config/spam_patterns.txt @@ -0,0 +1,97 @@ +.*\[\/link\] +.*\[\/url\] +51wisdom +acupuncturealliance +acyclovir +Adipex +adultfriend +airline +allegra +ampicill +anafranil +atenolol +attacke\.ch +autocorp +awardspace +blogspot\.com +bravehost\.com +butalbital +buy cheap +buy computer +buy-online +calling-phone-cards +casino +celexa +cialis +computer-exchange\.com +Cool website! +debt\s*consolidation +diazepam +display:\s*none +domaindlx\.com +equity\s*loan +Erectol +Feel free to visit my page +fortunecity +fuck +funpic\.de +gambling +Good job man +gucci +guestbook +hamburger +hold-em +holdem +home\s*loan +hoodia +http://[A-Za-z0-9_\.]+\.cn +hydrocodone +I am really excited +I really like your site +igotfree +ketoconazole +lust cartoon +mijneigenweblog +Mortage +my homepage +myspace +naked +netfirms\.com +nice site +overflow:\s*auto +paxil +pbwiki\.com +penis +Phentermine +phpbbforfree +pochta\.ru +poker +porn +prohosting +protonix +rapidforum +replica +ringtone +rolex +serotonin +singtaotor +slot\s*machin +soma +super site +texas +thepussies +tits +Tramadol +versace +viagra +vuitton +websamba\.com +xanax +xoomer +xrumer +Your site is great! +zoloft +\.iwarp\. +\.tripod\.com +\[link\= +\[url\= diff --git a/db/migrate/001_beta1_schema.rb b/db/migrate/001_beta1_schema.rb new file mode 100644 index 00000000..8985aa95 --- /dev/null +++ b/db/migrate/001_beta1_schema.rb @@ -0,0 +1,56 @@ +class Beta1Schema < ActiveRecord::Migration + def self.up + create_table "pages", :force => true do |t| + t.column "created_at", :datetime, :null => false + t.column "updated_at", :datetime, :null => false + t.column "web_id", :integer, :default => 0, :null => false + t.column "locked_by", :string, :limit => 60 + t.column "name", :string, :limit => 60 + t.column "locked_at", :datetime + end + + create_table "revisions", :force => true do |t| + t.column "created_at", :datetime, :null => false + t.column "updated_at", :datetime, :null => false + t.column "revised_at", :datetime, :null => false + t.column "page_id", :integer, :default => 0, :null => false + t.column "content", :text, :default => "", :null => false + t.column "author", :string, :limit => 60 + t.column "ip", :string, :limit => 60 + end + + create_table "system", :force => true do |t| + t.column "password", :string, :limit => 60 + end + + create_table "webs", :force => true do |t| + t.column "created_at", :datetime, :null => false + t.column "updated_at", :datetime, :null => false + t.column "name", :string, :limit => 60, :default => "", :null => false + t.column "address", :string, :limit => 60, :default => "", :null => false + t.column "password", :string, :limit => 60 + t.column "additional_style", :string + t.column "allow_uploads", :integer, :default => 1 + t.column "published", :integer, :default => 0 + t.column "count_pages", :integer, :default => 0 + t.column "markup", :string, :limit => 50, :default => "textile" + t.column "color", :string, :limit => 6, :default => "008B26" + t.column "max_upload_size", :integer, :default => 100 + t.column "safe_mode", :integer, :default => 0 + t.column "brackets_only", :integer, :default => 0 + end + + create_table "wiki_references", :force => true do |t| + t.column "created_at", :datetime, :null => false + t.column "updated_at", :datetime, :null => false + t.column "page_id", :integer, :default => 0, :null => false + t.column "referenced_name", :string, :limit => 60, :default => "", :null => false + t.column "link_type", :string, :limit => 1, :default => "", :null => false + end + end + + def self.down + raise 'Initial schema - cannot be further reverted' + end + +end diff --git a/db/migrate/002_beta2_changes_bulk.rb b/db/migrate/002_beta2_changes_bulk.rb new file mode 100644 index 00000000..b0b94486 --- /dev/null +++ b/db/migrate/002_beta2_changes_bulk.rb @@ -0,0 +1,36 @@ +class Beta2ChangesBulk < ActiveRecord::Migration + def self.up + add_index "revisions", "page_id" + add_index "revisions", "created_at" + add_index "revisions", "author" + + create_table "sessions", :force => true do |t| + t.column "session_id", :string + t.column "data", :text + t.column "updated_at", :datetime + end + add_index "sessions", "session_id" + + create_table "wiki_files", :force => true do |t| + t.column "created_at", :datetime, :null => false + t.column "updated_at", :datetime, :null => false + t.column "web_id", :integer, :null => false + t.column "file_name", :string, :null => false + t.column "description", :string, :null => false + end + + add_index "wiki_references", "page_id" + add_index "wiki_references", "referenced_name" + end + + def self.down + remove_index "wiki_references", "referenced_name" + remove_index "wiki_references", "page_id" + drop_table "wiki_files" + remove_index "sessions", "session_id" + drop_table "sessions" + remove_index "revisions", "author" + remove_index "revisions", "created_at" + remove_index "revisions", "page_id" + end +end diff --git a/instiki b/instiki new file mode 100755 index 00000000..db95d002 --- /dev/null +++ b/instiki @@ -0,0 +1,7 @@ +#!/bin/sh + +cd $(dirname $0) + +export LD_LIBRARY_PATH=./lib/native/linux-x86:$LD_LIBRARY_PATH +ruby script/server + diff --git a/instiki.cmd b/instiki.cmd new file mode 100644 index 00000000..00461495 --- /dev/null +++ b/instiki.cmd @@ -0,0 +1,2 @@ +set PATH=.\lib\native\win32;%PATH% +ruby.exe script\server -e production \ No newline at end of file diff --git a/instiki.rb b/instiki.rb new file mode 100755 index 00000000..93f6ea51 --- /dev/null +++ b/instiki.rb @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +load File.dirname(__FILE__) + '/script/server' diff --git a/lib/bluecloth_tweaked.rb b/lib/bluecloth_tweaked.rb new file mode 100644 index 00000000..b91622f1 --- /dev/null +++ b/lib/bluecloth_tweaked.rb @@ -0,0 +1,1127 @@ +#!/usr/bin/env ruby +# +# Bluecloth is a Ruby implementation of Markdown, a text-to-HTML conversion +# tool. +# +# == Synopsis +# +# doc = BlueCloth::new " +# ## Test document ## +# +# Just a simple test. +# " +# +# puts doc.to_html +# +# == Authors +# +# * Michael Granger +# +# == Contributors +# +# * Martin Chase - Peer review, helpful suggestions +# * Florian Gross - Filter options, suggestions +# +# == Copyright +# +# Original version: +# Copyright (c) 2003-2004 John Gruber +# +# All rights reserved. +# +# Ruby port: +# Copyright (c) 2004 The FaerieMUD Consortium. +# +# BlueCloth is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# BlueCloth is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# == To-do +# +# * Refactor some of the larger uglier methods that have to do their own +# brute-force scanning because of lack of Perl features in Ruby's Regexp +# class. Alternately, could add a dependency on 'pcre' and use most Perl +# regexps. +# +# * Put the StringScanner in the render state for thread-safety. +# +# == Version +# +# $Id: bluecloth.rb,v 1.3 2004/05/02 15:56:33 webster132 Exp $ +# + +require 'digest/md5' +require 'logger' +require 'strscan' + + +### BlueCloth is a Ruby implementation of Markdown, a text-to-HTML conversion +### tool. +class BlueCloth < String + + ### Exception class for formatting errors. + class FormatError < RuntimeError + + ### Create a new FormatError with the given source +str+ and an optional + ### message about the +specific+ error. + def initialize( str, specific=nil ) + if specific + msg = "Bad markdown format near %p: %s" % [ str, specific ] + else + msg = "Bad markdown format near %p" % str + end + + super( msg ) + end + end + + + # Release Version + Version = '0.0.3' + + # SVN Revision + SvnRev = %q$Rev: 37 $ + + # SVN Id tag + SvnId = %q$Id: bluecloth.rb,v 1.3 2004/05/02 15:56:33 webster132 Exp $ + + # SVN URL + SvnUrl = %q$URL: svn+ssh://cvs.faeriemud.org/var/svn/BlueCloth/trunk/lib/bluecloth.rb $ + + + # Rendering state struct. Keeps track of URLs, titles, and HTML blocks + # midway through a render. I prefer this to the globals of the Perl version + # because globals make me break out in hives. Or something. + RenderState = Struct::new( "RenderState", :urls, :titles, :html_blocks, :log ) + + # Tab width for #detab! if none is specified + TabWidth = 4 + + # The tag-closing string -- set to '>' for HTML + EmptyElementSuffix = "/>"; + + # Table of MD5 sums for escaped characters + EscapeTable = {} + '\\`*_{}[]()#.!'.split(//).each {|char| + hash = Digest::MD5::hexdigest( char ) + + EscapeTable[ char ] = { + :md5 => hash, + :md5re => Regexp::new( hash ), + :re => Regexp::new( '\\\\' + Regexp::escape(char) ), + } + } + + + ################################################################# + ### I N S T A N C E M E T H O D S + ################################################################# + + ### Create a new BlueCloth string. + def initialize( content="", *restrictions ) + @log = Logger::new( $deferr ) + @log.level = $DEBUG ? + Logger::DEBUG : + ($VERBOSE ? Logger::INFO : Logger::WARN) + @scanner = nil + + # Add any restrictions, and set the line-folding attribute to reflect + # what happens by default. + restrictions.flatten.each {|r| __send__("#{r}=", true) } + @fold_lines = true + + super( content ) + + @log.debug "String is: %p" % self + end + + + ###### + public + ###### + + # Filters for controlling what gets output for untrusted input. (But really, + # you're filtering bad stuff out of untrusted input at submission-time via + # untainting, aren't you?) + attr_accessor :filter_html, :filter_styles + + # RedCloth-compatibility accessor. Line-folding is part of Markdown syntax, + # so this isn't used by anything. + attr_accessor :fold_lines + + + ### Render Markdown-formatted text in this string object as HTML and return + ### it. The parameter is for compatibility with RedCloth, and is currently + ### unused, though that may change in the future. + def to_html( lite=false ) + + # Create a StringScanner we can reuse for various lexing tasks + @scanner = StringScanner::new( '' ) + + # Make a structure to carry around stuff that gets placeholdered out of + # the source. + rs = RenderState::new( {}, {}, {} ) + + # Make a copy of the string with normalized line endings, tabs turned to + # spaces, and a couple of guaranteed newlines at the end + text = self.gsub( /\r\n?/, "\n" ).detab + text += "\n\n" + @log.debug "Normalized line-endings: %p" % text + + # Filter HTML if we're asked to do so + if self.filter_html + text.gsub!( "<", "<" ) + text.gsub!( ">", ">" ) + @log.debug "Filtered HTML: %p" % text + end + + # Simplify blank lines + text.gsub!( /^ +$/, '' ) + @log.debug "Tabs -> spaces/blank lines stripped: %p" % text + + # Replace HTML blocks with placeholders + text = hide_html_blocks( text, rs ) + @log.debug "Hid HTML blocks: %p" % text + @log.debug "Render state: %p" % rs + + # Strip link definitions, store in render state + text = strip_link_definitions( text, rs ) + @log.debug "Stripped link definitions: %p" % text + @log.debug "Render state: %p" % rs + + # Escape meta-characters + text = escape_special_chars( text ) + @log.debug "Escaped special characters: %p" % text + + # Transform block-level constructs + text = apply_block_transforms( text, rs ) + @log.debug "After block-level transforms: %p" % text + + # Now swap back in all the escaped characters + text = unescape_special_chars( text ) + @log.debug "After unescaping special characters: %p" % text + + return text + end + + + ### Convert tabs in +str+ to spaces. + def detab( tabwidth=TabWidth ) + copy = self.dup + copy.detab!( tabwidth ) + return copy + end + + + ### Convert tabs to spaces in place and return self if any were converted. + def detab!( tabwidth=TabWidth ) + newstr = self.split( /\n/ ).collect {|line| + line.gsub( /(.*?)\t/ ) do + $1 + ' ' * (tabwidth - $1.length % tabwidth) + end + }.join("\n") + self.replace( newstr ) + end + + + ####### + #private + ####### + + ### Do block-level transforms on a copy of +str+ using the specified render + ### state +rs+ and return the results. + def apply_block_transforms( str, rs ) + # Port: This was called '_runBlockGamut' in the original + + @log.debug "Applying block transforms to:\n %p" % str + text = transform_headers( str, rs ) + text = transform_hrules( text, rs ) + text = transform_lists( text, rs ) + text = transform_code_blocks( text, rs ) + text = transform_block_quotes( text, rs ) + text = transform_auto_links( text, rs ) + text = hide_html_blocks( text, rs ) + + text = form_paragraphs( text, rs ) + + @log.debug "Done with block transforms:\n %p" % text + return text + end + + + ### Apply Markdown span transforms to a copy of the specified +str+ with the + ### given render state +rs+ and return it. + def apply_span_transforms( str, rs ) + @log.debug "Applying span transforms to:\n %p" % str + + str = transform_code_spans( str, rs ) + str = encode_html( str ) + str = transform_images( str, rs ) + str = transform_anchors( str, rs ) + str = transform_italic_and_bold( str, rs ) + + # Hard breaks + str.gsub!( / {2,}\n/, " + #
    + # tags for inner block must be indented. + #
    + #
    + StrictBlockRegex = %r{ + ^ # Start of line + <(#{BlockTagPattern}) # Start tag: \2 + \b # word break + (.*\n)*? # Any number of lines, minimal match + # Matching end tag + [ ]* # trailing spaces + (?=\n+|\Z) # End of line or document + }ix + + # More-liberal block-matching + LooseBlockRegex = %r{ + ^ # Start of line + <(#{BlockTagPattern}) # start tag: \2 + \b # word break + (.*\n)*? # Any number of lines, minimal match + .* # Anything + Matching end tag + [ ]* # trailing spaces + (?=\n+|\Z) # End of line or document + }ix + + # Special case for
    . + HruleBlockRegex = %r{ + ( # $1 + \A\n? # Start of doc + optional \n + | # or + .*\n\n # anything + blank line + ) + ( # save in $2 + [ ]* # Any spaces +
    ])*? # Attributes + /?> # Tag close + (?=\n\n|\Z) # followed by a blank line or end of document + ) + }ix + + ### Replace all blocks of HTML in +str+ that start in the left margin with + ### tokens. + def hide_html_blocks( str, rs ) + @log.debug "Hiding HTML blocks in %p" % str + + # Tokenizer proc to pass to gsub + tokenize = lambda {|match| + key = Digest::MD5::hexdigest( match ) + rs.html_blocks[ key ] = match + @log.debug "Replacing %p with %p" % + [ match, key ] + "\n\n#{key}\n\n" + } + + rval = str.dup + + @log.debug "Finding blocks with the strict regex..." + rval.gsub!( StrictBlockRegex, &tokenize ) + + @log.debug "Finding blocks with the loose regex..." + rval.gsub!( LooseBlockRegex, &tokenize ) + + @log.debug "Finding hrules..." + rval.gsub!( HruleBlockRegex ) {|match| $1 + tokenize[$2] } + + return rval + end + + + # Link defs are in the form: ^[id]: url "optional title" + LinkRegex = %r{ + ^[ ]*\[(.+)\]: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (\S+) # url = $2 + [ ]* + \n? # maybe one newline + [ ]* + (?: + # Titles are delimited by "quotes" or (parens). + ["(] + (.+?) # title = $3 + [")] # Matching ) or " + [ ]* + )? # title is optional + (?:\n+|\Z) + }x + + ### Strip link definitions from +str+, storing them in the given RenderState + ### +rs+. + def strip_link_definitions( str, rs ) + str.gsub( LinkRegex ) {|match| + id, url, title = $1, $2, $3 + + rs.urls[ id.downcase ] = encode_html( url ) + unless title.nil? + rs.titles[ id.downcase ] = title.gsub( /"/, """ ) + end + "" + } + end + + + ### Escape special characters in the given +str+ + def escape_special_chars( str ) + @log.debug " Escaping special characters" + text = '' + + tokenize_html( str ) {|token, str| + @log.debug " Adding %p token %p" % [ token, str ] + case token + + # Within tags, encode * and _ + when :tag + text += str. + gsub( /\*/, EscapeTable['*'][:md5] ). + gsub( /_/, EscapeTable['_'][:md5] ) + + # Encode backslashed stuff in regular text + when :text + text += encode_backslash_escapes( str ) + else + raise TypeError, "Unknown token type %p" % token + end + } + + @log.debug " Text with escapes is now: %p" % text + return text + end + + + ### Swap escaped special characters in a copy of the given +str+ and return + ### it. + def unescape_special_chars( str ) + EscapeTable.each {|char, hash| + @log.debug "Unescaping escaped %p with %p" % + [ char, hash[:md5re] ] + str.gsub!( hash[:md5re], char ) + } + + return str + end + + + ### Return a copy of the given +str+ with any backslashed special character + ### in it replaced with MD5 placeholders. + def encode_backslash_escapes( str ) + # Make a copy with any double-escaped backslashes encoded + text = str.gsub( /\\\\/, EscapeTable['\\'][:md5] ) + + EscapeTable.each_pair {|char, esc| + next if char == '\\' + text.gsub!( esc[:re], esc[:md5] ) + } + + return text + end + + + ### Transform any Markdown-style horizontal rules in a copy of the specified + ### +str+ and return it. + def transform_hrules( str, rs ) + @log.debug " Transforming horizontal rules" + str.gsub( /^( ?[\-\*] ?){3,}$/, "\n\n%s\n} % [ + list_type, + transform_list_items( list, rs ), + list_type, + ] + } + end + + + # Pattern for transforming list items + ListItemRegexp = %r{ + (\n)? # leading line = $1 + (^[ ]*) # leading whitespace = $2 + (\*|\d+\.) [ ]+ # list marker = $3 + ((?m:.+?) # list item text = $4 + (\n{1,2})) + (?= \n* (\z | \2 (\*|\d+\.) [ ]+)) + }x + + ### Transform list items in a copy of the given +str+ and return it. + def transform_list_items( str, rs ) + @log.debug " Transforming list items" + + # Trim trailing blank lines + str = str.sub( /\n{2,}\z/, "\n" ) + + str.gsub( ListItemRegexp ) {|line| + @log.debug " Found item line %p" % line + leading_line, item = $1, $4 + + if leading_line or /\n{2,}/.match( item ) + @log.debug " Found leading line or item has a blank" + item = apply_block_transforms( outdent(item), rs ) + else + # Recursion for sub-lists + @log.debug " Recursing for sublist" + item = transform_lists( outdent(item), rs ).chomp + item = apply_span_transforms( item, rs ) + end + + %{
  • %s
  • \n} % item + } + end + + + # Pattern for matching codeblocks + CodeBlockRegexp = %r{ + (.?) # $1 = preceding character + :\n+ # colon + NL delimiter + ( # $2 = the code block + (?: + (?:[ ]{#{TabWidth}} | \t) # a tab or tab-width of spaces + .*\n+ + )+ + ) + ((?=^[ ]{0,#{TabWidth}}\S)|\Z) # Lookahead for non-space at + # line-start, or end of doc + }x + + ### Transform Markdown-style codeblocks in a copy of the specified +str+ and + ### return it. + def transform_code_blocks( str, rs ) + @log.debug " Transforming code blocks" + + str.gsub( CodeBlockRegexp ) {|block| + prevchar, codeblock = $1, $2 + + @log.debug " prevchar = %p" % prevchar + + # Generated the codeblock + %{%s\n\n
    %s\n
    \n\n} % [ + (prevchar.empty? || /\s/ =~ prevchar) ? "" : "#{prevchar}:", + encode_code( outdent(codeblock), rs ).rstrip, + ] + } + end + + + # Pattern for matching Markdown blockquote blocks + BlockQuoteRegexp = %r{ + (?: + ^[ ]*>[ ]? # '>' at the start of a line + .+\n # rest of the first line + (?:.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + }x + + ### Transform Markdown-style blockquotes in a copy of the specified +str+ + ### and return it. + def transform_block_quotes( str, rs ) + @log.debug " Transforming block quotes" + + str.gsub( BlockQuoteRegexp ) {|quote| + @log.debug "Making blockquote from %p" % quote + quote.gsub!( /^[ ]*>[ ]?/, '' ) + %{
    \n%s\n
    \n\n} % + apply_block_transforms( quote, rs ). + gsub( /^/, " " * TabWidth ) + } + end + + + AutoAnchorURLRegexp = /<((https?|ftp):[^'">\s]+)>/ + AutoAnchorEmailRegexp = %r{ + < + ( + [-.\w]+ + \@ + [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ + ) + > + }x + + ### Transform URLs in a copy of the specified +str+ into links and return + ### it. + def transform_auto_links( str, rs ) + @log.debug " Transforming auto-links" + str.gsub( AutoAnchorURLRegexp, %{\\1}). + gsub( AutoAnchorEmailRegexp ) {|addr| + encode_email_address( unescape_special_chars($1) ) + } + end + + + # Encoder functions to turn characters of an email address into encoded + # entities. + Encoders = [ + lambda {|char| "&#%03d;" % char}, + lambda {|char| "&#x%X;" % char}, + lambda {|char| char.chr }, + ] + + ### Transform a copy of the given email +addr+ into an escaped version safer + ### for posting publicly. + def encode_email_address( addr ) + + rval = '' + ("mailto:" + addr).each_byte {|b| + case b + when ?: + rval += ":" + when ?@ + rval += Encoders[ rand(2) ][ b ] + else + r = rand(100) + rval += ( + r > 90 ? Encoders[2][ b ] : + r < 45 ? Encoders[1][ b ] : + Encoders[0][ b ] + ) + end + } + + return %{%s} % [ rval, rval.sub(/.+?:/, '') ] + end + + + # Regex for matching Setext-style headers + SetextHeaderRegexp = %r{ + (.+) # The title text ($1) + \n + ([\-=])+ # Match a line of = or -. Save only one in $2. + [ ]*\n+ + }x + + # Regexp for matching ATX-style headers + AtxHeaderRegexp = %r{ + ^(\#{1,6}) # $1 = string of #'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #'s (not counted) + \n+ + }x + + ### Apply Markdown header transforms to a copy of the given +str+ amd render + ### state +rs+ and return the result. + def transform_headers( str, rs ) + @log.debug " Transforming headers" + + # Setext-style headers: + # Header 1 + # ======== + # + # Header 2 + # -------- + # + str. + gsub( SetextHeaderRegexp ) {|m| + @log.debug "Found setext-style header" + title, hdrchar = $1, $2 + title = apply_span_transforms( title, rs ) + + case hdrchar + when '=' + %[

    #{title}

    \n\n] + when '-' + %[

    #{title}

    \n\n] + else + title + end + }. + + gsub( AtxHeaderRegexp ) {|m| + @log.debug "Found ATX-style header" + hdrchars, title = $1, $2 + title = apply_span_transforms( title, rs ) + + level = hdrchars.length + %{%s\n\n} % [ level, title, level ] + } + end + + + ### Wrap all remaining paragraph-looking text in a copy of +str+ inside

    + ### tags and return it. + def form_paragraphs( str, rs ) + @log.debug " Forming paragraphs" + grafs = str. + sub( /\A\n+/, '' ). + sub( /\n+\z/, '' ). + split( /\n{2,}/ ) + + rval = grafs.collect {|graf| + + # Unhashify HTML blocks if this is a placeholder + if rs.html_blocks.key?( graf ) + rs.html_blocks[ graf ] + + # Otherwise, wrap in

    tags + else + apply_span_transforms(graf, rs). + sub( /^[ ]*/, '

    ' ) + '

    ' + end + }.join( "\n\n" ) + + @log.debug " Formed paragraphs: %p" % rval + return rval + end + + + # Pattern to match the linkid part of an anchor tag for reference-style + # links. + RefLinkIdRegex = %r{ + [ ]? # Optional leading space + (?:\n[ ]*)? # Optional newline + spaces + \[ + (.*?) # Id = $1 + \] + }x + + InlineLinkRegex = %r{ + \( # Literal paren + [ ]* # Zero or more spaces + (.*?) # URI = $1 + [ ]* # Zero or more spaces + (?: # + ([\"\']) # Opening quote char = $2 + (.*?) # Title = $3 + \2 # Matching quote char + )? # Title is optional + \) + }x + + ### Apply Markdown anchor transforms to a copy of the specified +str+ with + ### the given render state +rs+ and return it. + def transform_anchors( str, rs ) + @log.debug " Transforming anchors" + @scanner.string = str.dup + text = '' + + # Scan the whole string + until @scanner.empty? + + if @scanner.scan( /\[/ ) + link = ''; linkid = '' + depth = 1 + startpos = @scanner.pos + @log.debug " Found a bracket-open at %d" % startpos + + # Scan the rest of the tag, allowing unlimited nested []s. If + # the scanner runs out of text before the opening bracket is + # closed, append the text and return (wasn't a valid anchor). + while depth.nonzero? + linktext = @scanner.scan_until( /\]|\[/ ) + + if linktext + @log.debug " Found a bracket at depth %d: %p" % + [ depth, linktext ] + link += linktext + + # Decrement depth for each closing bracket + depth += ( linktext[-1, 1] == ']' ? -1 : 1 ) + @log.debug " Depth is now #{depth}" + + # If there's no more brackets, it must not be an anchor, so + # just abort. + else + @log.debug " Missing closing brace, assuming non-link." + link += @scanner.rest + @scanner.terminate + return text + '[' + link + end + end + link.slice!( -1 ) # Trim final ']' + @log.debug " Found leading link %p" % link + + # Look for a reference-style second part + if @scanner.scan( RefLinkIdRegex ) + linkid = @scanner[1] + linkid = link.dup if linkid.empty? + linkid.downcase! + @log.debug " Found a linkid: %p" % linkid + + # If there's a matching link in the link table, build an + # anchor tag for it. + if rs.urls.key?( linkid ) + @log.debug " Found link key in the link table: %p" % + rs.urls[linkid] + url = escape_md( rs.urls[linkid] ) + + text += %{#{link}} + + # If the link referred to doesn't exist, just append the raw + # source to the result + else + @log.debug " Linkid %p not found in link table" % linkid + @log.debug " Appending original string instead: %p" % + @scanner.string[ startpos-1 .. @scanner.pos ] + text += @scanner.string[ startpos-1 .. @scanner.pos ] + end + + # ...or for an inline style second part + elsif @scanner.scan( InlineLinkRegex ) + url = @scanner[1] + title = @scanner[3] + @log.debug " Found an inline link to %p" % url + + text += %{#{link}} + + # No linkid part: just append the first part as-is. + else + @log.debug "No linkid, so no anchor. Appending literal text." + text += @scanner.string[ startpos-1 .. @scanner.pos-1 ] + end # if linkid + + # Plain text + else + @log.debug " Scanning to the next link from %p" % @scanner.rest + text += @scanner.scan( /[^\[]+/ ) + end + + end # until @scanner.empty? + + return text + end + + # Pattern to match strong emphasis in Markdown text + BoldRegexp = %r{ (\*\*|__) (?=\S) (.+?\S) \1 }x + + # Pattern to match normal emphasis in Markdown text + ItalicRegexp = %r{ (\*|_) (?=\S) (.+?\S) \1 }x + + ### Transform italic- and bold-encoded text in a copy of the specified +str+ + ### and return it. + def transform_italic_and_bold( str, rs ) + @log.debug " Transforming italic and bold" + + str. + gsub( BoldRegexp, %{\\2} ). + gsub( ItalicRegexp, %{\\2} ) + end + + + ### Transform backticked spans into spans. + def transform_code_spans( str, rs ) + @log.debug " Transforming code spans" + + # Set up the string scanner and just return the string unless there's at + # least one backtick. + @scanner.string = str.dup + unless @scanner.exist?( /`/ ) + @scanner.terminate + @log.debug "No backticks found for code span in %p" % str + return str + end + + @log.debug "Transforming code spans in %p" % str + + # Build the transformed text anew + text = '' + + # Scan to the end of the string + until @scanner.empty? + + # Scan up to an opening backtick + if pre = @scanner.scan_until( /.?(?=`)/m ) + text += pre + @log.debug "Found backtick at %d after '...%s'" % + [ @scanner.pos, text[-10, 10] ] + + # Make a pattern to find the end of the span + opener = @scanner.scan( /`+/ ) + len = opener.length + closer = Regexp::new( opener ) + @log.debug "Scanning for end of code span with %p" % closer + + # Scan until the end of the closing backtick sequence. Chop the + # backticks off the resultant string, strip leading and trailing + # whitespace, and encode any enitites contained in it. + codespan = @scanner.scan_until( closer ) or + raise FormatError::new( @scanner.rest[0,20], + "No %p found before end" % opener ) + + @log.debug "Found close of code span at %d: %p" % + [ @scanner.pos - len, codespan ] + codespan.slice!( -len, len ) + text += "%s" % + encode_code( codespan.strip, rs ) + + # If there's no more backticks, just append the rest of the string + # and move the scan pointer to the end + else + text += @scanner.rest + @scanner.terminate + end + end + + return text + end + + + # Next, handle inline images: ![alt text](url "optional title") + # Don't forget: encode * and _ + InlineImageRegexp = %r{ + ( # Whole match = $1 + !\[ (.*?) \] # alt text = $2 + \([ ]* (\S+) [ ]* # source url = $3 + ( # title = $4 + (["']) # quote char = $5 + .*? + \5 # matching quote + [ ]* + )? # title is optional + \) + ) + }xs #" + + + # Reference-style images + ReferenceImageRegexp = %r{ + ( # Whole match = $1 + !\[ (.*?) \] # Alt text = $2 + [ ]? # Optional space + (?:\n[ ]*)? # One optional newline + spaces + \[ (.*?) \] # id = $3 + ) + }xs + + ### Turn image markup into image tags. + def transform_images( str, rs ) + @log.debug " Transforming images" % str + + # Handle reference-style labeled images: ![alt text][id] + str. + gsub( ReferenceImageRegexp ) {|match| + whole, alt, linkid = $1, $2, $3.downcase + @log.debug "Matched %p" % match + res = nil + + # for shortcut links like ![this][]. + linkid = alt.downcase if linkid.empty? + + if rs.urls.key?( linkid ) + url = escape_md( rs.urls[linkid] ) + @log.debug "Found url '%s' for linkid '%s' " % + [ url, linkid ] + + # Build the tag + result = %{%s}, '>' ). + gsub( CodeEscapeRegexp ) {|match| EscapeTable[match][:md5]} + end + + + + ################################################################# + ### U T I L I T Y F U N C T I O N S + ################################################################# + + ### Escape any markdown characters in a copy of the given +str+ and return + ### it. + def escape_md( str ) + str. + gsub( /\*/, '*' ). + gsub( /_/, '_' ) + end + + + # Matching constructs for tokenizing X/HTML + HTMLCommentRegexp = %r{ }mx + XMLProcInstRegexp = %r{ <\? .*? \?> }mx + MetaTag = Regexp::union( HTMLCommentRegexp, XMLProcInstRegexp ) + + HTMLTagOpenRegexp = %r{ < [a-z/!$] [^<>]* }mx + HTMLTagCloseRegexp = %r{ > }x + HTMLTagPart = Regexp::union( HTMLTagOpenRegexp, HTMLTagCloseRegexp ) + + ### Break the HTML source in +str+ into a series of tokens and return + ### them. The tokens are just 2-element Array tuples with a type and the + ### actual content. If this function is called with a block, the type and + ### text parts of each token will be yielded to it one at a time as they are + ### extracted. + def tokenize_html( str ) + depth = 0 + tokens = [] + @scanner.string = str.dup + type, token = nil, nil + + until @scanner.empty? + @log.debug "Scanning from %p" % @scanner.rest + + # Match comments and PIs without nesting + if (( token = @scanner.scan(MetaTag) )) + type = :tag + + # Do nested matching for HTML tags + elsif (( token = @scanner.scan(HTMLTagOpenRegexp) )) + tagstart = @scanner.pos + @log.debug " Found the start of a plain tag at %d" % tagstart + + # Start the token with the opening angle + depth = 1 + type = :tag + + # Scan the rest of the tag, allowing unlimited nested <>s. If + # the scanner runs out of text before the tag is closed, raise + # an error. + while depth.nonzero? + + # Scan either an opener or a closer + chunk = @scanner.scan( HTMLTagPart ) or + raise "Malformed tag at character %d: %p" % + [ tagstart, token + @scanner.rest ] + + @log.debug " Found another part of the tag at depth %d: %p" % + [ depth, chunk ] + + token += chunk + + # If the last character of the token so far is a closing + # angle bracket, decrement the depth. Otherwise increment + # it for a nested tag. + depth += ( token[-1, 1] == '>' ? -1 : 1 ) + @log.debug " Depth is now #{depth}" + end + + # Match text segments + else + @log.debug " Looking for a chunk of text" + type = :text + + # Scan forward, always matching at least one character to move + # the pointer beyond any non-tag '<'. + token = @scanner.scan_until( /[^<]+/m ) + end + + @log.debug " type: %p, token: %p" % [ type, token ] + + # If a block is given, feed it one token at a time. Add the token to + # the token list to be returned regardless. + if block_given? + yield( type, token ) + end + tokens << [ type, token ] + end + + return tokens + end + + + ### Return a copy of +str+ with angle brackets and ampersands HTML-encoded. + def encode_html( str ) + str.gsub( /&(?!#?[x]?(?:[0-9a-f]+|\w{1,8});)/i, "&" ). + gsub( %r{<(?![a-z/?\$!])}i, "<" ) + end + + + ### Return one level of line-leading tabs or spaces from a copy of +str+ and + ### return it. + def outdent( str ) + str.gsub( /^(\t|[ ]{1,#{TabWidth}})/, '') + end + +end # class BlueCloth + diff --git a/lib/chunks/category.rb b/lib/chunks/category.rb new file mode 100644 index 00000000..d08d8636 --- /dev/null +++ b/lib/chunks/category.rb @@ -0,0 +1,33 @@ +require 'chunks/chunk' + +# The category chunk looks for "category: news" on a line by +# itself and parses the terms after the ':' as categories. +# Other classes can search for Category chunks within +# rendered content to find out what categories this page +# should be in. +# +# Category lines can be hidden using ':category: news', for example +class Category < Chunk::Abstract + CATEGORY_PATTERN = /^(:)?category\s*:(.*)$/i + def self.pattern() CATEGORY_PATTERN end + + attr_reader :hidden, :list + +def initialize(match_data, content) + super(match_data, content) + @hidden = match_data[1] + @list = match_data[2].split(',').map { |c| c.strip } + @unmask_text = '' + if @hidden + @unmask_text = '' + else + category_urls = @list.map { |category| url(category) }.join(', ') + @unmask_text = '
    category: ' + category_urls + '
    ' + end + end + + # TODO move presentation of page metadata to controller/view + def url(category) + %{#{category}} + end +end diff --git a/lib/chunks/chunk.rb b/lib/chunks/chunk.rb new file mode 100644 index 00000000..18de7d0c --- /dev/null +++ b/lib/chunks/chunk.rb @@ -0,0 +1,79 @@ +require 'uri/common' + +# A chunk is a pattern of text that can be protected +# and interrogated by a renderer. Each Chunk class has a +# +pattern+ that states what sort of text it matches. +# Chunks are initalized by passing in the result of a +# match by its pattern. + +module Chunk + class Abstract + + # automatically construct the array of derivatives of Chunk::Abstract + @derivatives = [] + + class << self + attr_reader :derivatives + end + + def self::inherited( klass ) + Abstract::derivatives << klass + end + + # the class name part of the mask strings + def self.mask_string + self.to_s.delete(':').downcase + end + + # a regexp that matches all chunk_types masks + def Abstract::mask_re(chunk_types) + chunk_classes = chunk_types.map{|klass| klass.mask_string}.join("|") + /chunk(-?\d+)(#{chunk_classes})chunk/ + end + + attr_reader :text, :unmask_text, :unmask_mode + + def initialize(match_data, content) + @text = match_data[0] + @content = content + @unmask_mode = :normal + end + + # Find all the chunks of the given type in content + # Each time the pattern is matched, create a new + # chunk for it, and replace the occurance of the chunk + # in this content with its mask. + def self.apply_to(content) + content.gsub!( self.pattern ) do |match| + new_chunk = self.new($~, content) + content.add_chunk(new_chunk) + new_chunk.mask + end + end + + # should contain only [a-z0-9] + def mask + @mask ||= "chunk#{self.object_id}#{self.class.mask_string}chunk" + end + + def unmask + @content.sub!(mask, @unmask_text) + end + + def rendered? + @unmask_mode == :normal + end + + def escaped? + @unmask_mode == :escape + end + + def revert + @content.sub!(mask, @text) + # unregister + @content.delete_chunk(self) + end + + end + +end diff --git a/lib/chunks/engines.rb b/lib/chunks/engines.rb new file mode 100644 index 00000000..3b7b5bbe --- /dev/null +++ b/lib/chunks/engines.rb @@ -0,0 +1,62 @@ +$: << File.dirname(__FILE__) + "../../lib" + +require_dependency 'chunks/chunk' + +# The markup engines are Chunks that call the one of RedCloth +# or RDoc to convert text. This markup occurs when the chunk is required +# to mask itself. +module Engines + class AbstractEngine < Chunk::Abstract + + # Create a new chunk for the whole content and replace it with its mask. + def self.apply_to(content) + new_chunk = self.new(content) + content.replace(new_chunk.mask) + end + + private + + # Never create engines by constructor - use apply_to instead + def initialize(content) + @content = content + end + + end + + class Textile < AbstractEngine + def mask + require_dependency 'redcloth' + redcloth = RedCloth.new(@content, [:hard_breaks] + @content.options[:engine_opts]) + redcloth.filter_html = false + redcloth.no_span_caps = false + redcloth.to_html(:textile) + end + end + + class Markdown < AbstractEngine + def mask + require_dependency 'bluecloth_tweaked' + BlueCloth.new(@content, @content.options[:engine_opts]).to_html + end + end + + class Mixed < AbstractEngine + def mask + require_dependency 'redcloth' + redcloth = RedCloth.new(@content, @content.options[:engine_opts]) + redcloth.filter_html = false + redcloth.no_span_caps = false + redcloth.to_html + end + end + + class RDoc < AbstractEngine + def mask + require_dependency 'rdocsupport' + RDocSupport::RDocFormatter.new(@content).to_html + end + end + + MAP = { :textile => Textile, :markdown => Markdown, :mixed => Mixed, :rdoc => RDoc } + MAP.default = Textile +end diff --git a/lib/chunks/include.rb b/lib/chunks/include.rb new file mode 100644 index 00000000..ac9b9bd8 --- /dev/null +++ b/lib/chunks/include.rb @@ -0,0 +1,49 @@ +require 'chunks/wiki' + +# Includes the contents of another page for rendering. +# The include command looks like this: "[[!include PageName]]". +# It is a WikiReference since it refers to another page (PageName) +# and the wiki content using this command must be notified +# of changes to that page. +# If the included page could not be found, a warning is displayed. + +class Include < WikiChunk::WikiReference + + INCLUDE_PATTERN = /\[\[!include\s+(.*?)\]\]\s*/i + def self.pattern() INCLUDE_PATTERN end + + def initialize(match_data, content) + super + @page_name = match_data[1].strip + rendering_mode = content.options[:mode] || :show + @unmask_text = get_unmask_text_avoiding_recursion_loops(rendering_mode) + end + + private + + def get_unmask_text_avoiding_recursion_loops(rendering_mode) + if refpage + # TODO This way of instantiating a renderer is ugly. + renderer = PageRenderer.new(refpage.current_revision) + if renderer.wiki_includes.include?(@content.page_name) + # this will break the recursion + @content.delete_chunk(self) + return "Recursive include detected; #{@page_name} --> #{@content.page_name} " + + "--> #{@page_name}\n" + else + included_content = + case rendering_mode + when :show then renderer.display_content + when :publish then renderer.display_published + when :export then renderer.display_content_for_export + else raise "Unsupported rendering mode #{@mode.inspect}" + end + @content.merge_chunks(included_content) + return included_content.pre_rendered + end + else + return "Could not include #{@page_name}\n" + end + end + +end diff --git a/lib/chunks/literal.rb b/lib/chunks/literal.rb new file mode 100644 index 00000000..09da4005 --- /dev/null +++ b/lib/chunks/literal.rb @@ -0,0 +1,31 @@ +require 'chunks/chunk' + +# These are basic chunks that have a pattern and can be protected. +# They are used by rendering process to prevent wiki rendering +# occuring within literal areas such as and
     blocks
    +# and within HTML tags.
    +module Literal
    +
    +  class AbstractLiteral < Chunk::Abstract
    +
    +    def initialize(match_data, content)
    +      super
    +      @unmask_text = @text
    +    end
    +
    +  end
    +
    +  # A literal chunk that protects 'code' and 'pre' tags from wiki rendering.
    +  class Pre < AbstractLiteral
    +    PRE_BLOCKS = "a|pre|code"
    +    PRE_PATTERN = Regexp.new('<('+PRE_BLOCKS+')\b[^>]*?>.*?', Regexp::MULTILINE)
    +    def self.pattern() PRE_PATTERN end
    +  end 
    +
    +  # A literal chunk that protects HTML tags from wiki rendering.
    +  class Tags < AbstractLiteral
    +    TAGS = "a|img|em|strong|div|span|table|td|th|ul|ol|li|dl|dt|dd"
    +    TAGS_PATTERN = Regexp.new('<(?:'+TAGS+')[^>]*?>', Regexp::MULTILINE) 
    +    def self.pattern() TAGS_PATTERN  end
    +  end
    +end
    diff --git a/lib/chunks/nowiki.rb b/lib/chunks/nowiki.rb
    new file mode 100644
    index 00000000..ef99ec0b
    --- /dev/null
    +++ b/lib/chunks/nowiki.rb
    @@ -0,0 +1,28 @@
    +require 'chunks/chunk'
    +
    +# This chunks allows certain parts of a wiki page to be hidden from the
    +# rest of the rendering pipeline. It should be run at the beginning
    +# of the pipeline in `wiki_content.rb`.
    +#
    +# An example use of this chunk is to markup double brackets or
    +# auto URI links:
    +#  Here are [[double brackets]] and a URI: www.uri.org
    +#
    +# The contents of the chunks will not be processed by any other chunk
    +# so the `www.uri.org` and the double brackets will appear verbatim.
    +#
    +# Author: Mark Reid 
    +# Created: 8th June 2004
    +class NoWiki < Chunk::Abstract
    +
    +  NOWIKI_PATTERN = Regexp.new('(.*?)', Regexp::MULTILINE)
    +  def self.pattern() NOWIKI_PATTERN end
    +
    +  attr_reader :plain_text
    +
    +  def initialize(match_data, content)
    +    super
    +    @plain_text = @unmask_text = match_data[1]
    +  end
    +
    +end
    diff --git a/lib/chunks/test.rb b/lib/chunks/test.rb
    new file mode 100644
    index 00000000..73af8142
    --- /dev/null
    +++ b/lib/chunks/test.rb
    @@ -0,0 +1,18 @@
    +require 'test/unit'
    +
    +class ChunkTest < Test::Unit::TestCase
    +
    +  # Asserts a number of tests for the given type and text.
    +  def match(type, test_text, expected)
    +    pattern = type.pattern
    +    assert_match(pattern, test_text)
    +    pattern =~ test_text   # Previous assertion guarantees match
    +    chunk = type.new($~)
    +    
    +    # Test if requested parts are correct.
    +    for method_sym, value in expected do
    +      assert_respond_to(chunk, method_sym)
    +      assert_equal(value, chunk.method(method_sym).call, "Checking value of '#{method_sym}'")
    +    end
    +  end
    +end
    diff --git a/lib/chunks/uri.rb b/lib/chunks/uri.rb
    new file mode 100644
    index 00000000..1a208535
    --- /dev/null
    +++ b/lib/chunks/uri.rb
    @@ -0,0 +1,182 @@
    +require 'chunks/chunk'
    +
    +# This wiki chunk matches arbitrary URIs, using patterns from the Ruby URI modules.
    +# It parses out a variety of fields that could be used by renderers to format
    +# the links in various ways (shortening domain names, hiding email addresses)
    +# It matches email addresses and host.com.au domains without schemes (http://)
    +# but adds these on as required.
    +#
    +# The heuristic used to match a URI is designed to err on the side of caution.
    +# That is, it is more likely to not autolink a URI than it is to accidently
    +# autolink something that is not a URI. The reason behind this is it is easier
    +# to force a URI link by prefixing 'http://' to it than it is to escape and
    +# incorrectly marked up non-URI.
    +#
    +# I'm using a part of the [ISO 3166-1 Standard][iso3166] for country name suffixes.
    +# The generic names are from www.bnoack.com/data/countrycode2.html)
    +#   [iso3166]: http://geotags.com/iso3166/
    +
    +class URIChunk < Chunk::Abstract
    +  include URI::REGEXP::PATTERN
    +
    +  # this condition is to get rid of pesky warnings in tests
    +  unless defined? URIChunk::INTERNET_URI_REGEXP
    +
    +    GENERIC = 'aero|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org'
    +    
    +    COUNTRY = 'ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|az|ba|bb|bd|be|' + 
    +      'bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cf|cd|cg|ch|ci|ck|cl|' + 
    +      'cm|cn|co|cr|cs|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|fi|' + 
    +      'fj|fk|fm|fo|fr|fx|ga|gb|gd|ge|gf|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|' + 
    +      'hk|hm|hn|hr|ht|hu|id|ie|il|in|io|iq|ir|is|it|jm|jo|jp|ke|kg|kh|ki|km|kn|' + 
    +      'kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|mg|mh|mk|ml|mm|' + 
    +      'mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nt|' + 
    +      'nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|' + 
    +      'sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|' + 
    +      'tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|' + 
    +      'ws|ye|yt|yu|za|zm|zr|zw'
    +    # These are needed otherwise HOST will match almost anything
    +    TLDS = "(?:#{GENERIC}|#{COUNTRY})"
    +    
    +    # Redefine USERINFO so that it must have non-zero length
    +    USERINFO = "(?:[#{UNRESERVED};:&=+$,]|#{ESCAPED})+"
    +  
    +    # unreserved_no_ending = alphanum | mark, but URI_ENDING [)!] excluded
    +    UNRESERVED_NO_ENDING = "-_.~*'(#{ALNUM}"  
    +
    +    # this ensures that query or fragment do not end with URI_ENDING
    +    # and enable us to use a much simpler self.pattern Regexp
    +
    +    # uric_no_ending = reserved | unreserved_no_ending | escaped
    +    URIC_NO_ENDING = "(?:[#{UNRESERVED_NO_ENDING}#{RESERVED}]|#{ESCAPED})"
    +    # query = *uric
    +    QUERY = "#{URIC_NO_ENDING}*"
    +    # fragment = *uric
    +    FRAGMENT = "#{URIC_NO_ENDING}*"
    +
    +    # DOMLABEL is defined in the ruby uri library, TLDS is defined above
    +    INTERNET_HOSTNAME = "(?:#{DOMLABEL}\\.)+#{TLDS}" 
    +
    +    # Correct a typo bug in ruby 1.8.x lib/uri/common.rb 
    +    PORT = '\\d*'
    +
    +    INTERNET_URI =
    +        "(?:(#{SCHEME}):/{0,2})?" +   # Optional scheme:        (\1)
    +        "(?:(#{USERINFO})@)?" +       # Optional userinfo@      (\2)
    +        "(#{INTERNET_HOSTNAME})" +    # Mandatory hostname      (\3)
    +        "(?::(#{PORT}))?" +           # Optional :port          (\4)
    +        "(#{ABS_PATH})?"  +           # Optional absolute path  (\5)
    +        "(?:\\?(#{QUERY}))?" +        # Optional ?query         (\6)
    +        "(?:\\#(#{FRAGMENT}))?"  +    # Optional #fragment      (\7)
    +        '(?=\.?(?:\s|\)|\z))'         # ends only with optional dot + space or ")" 
    +                                      # or end of the string
    +
    +    SUSPICIOUS_PRECEDING_CHARACTER = '(!|\"\:|\"|\\\'|\]\()?'  # any of !, ":, ", ', ](
    +  
    +    INTERNET_URI_REGEXP = 
    +        Regexp.new(SUSPICIOUS_PRECEDING_CHARACTER + INTERNET_URI, Regexp::EXTENDED, 'N')
    +
    +  end
    +
    +  def URIChunk.pattern
    +    INTERNET_URI_REGEXP
    +  end
    +
    +  attr_reader :user, :host, :port, :path, :query, :fragment, :link_text
    +  
    +  def self.apply_to(content)
    +    content.gsub!( self.pattern ) do |matched_text|
    +      chunk = self.new($~, content)
    +      if chunk.avoid_autolinking?
    +        # do not substitute nor register the chunk
    +        matched_text
    +      else
    +        content.add_chunk(chunk)
    +        chunk.mask
    +      end
    +    end
    +  end
    +
    +  def initialize(match_data, content)
    +    super
    +    @link_text = match_data[0]
    +    @suspicious_preceding_character = match_data[1]
    +    @original_scheme, @user, @host, @port, @path, @query, @fragment = match_data[2..-1]
    +    treat_trailing_character
    +    @unmask_text = "#{link_text}"
    +  end
    +
    +  def avoid_autolinking?
    +    not @suspicious_preceding_character.nil?
    +  end
    +
    +  def treat_trailing_character
    +    # If the last character matched by URI pattern is in ! or ), this may be part of the markup,
    +    # not a URL. We should handle it as such. It is possible to do it by a regexp, but 
    +    # much easier to do programmatically
    +    last_char = @link_text[-1..-1]
    +    if last_char == ')' or last_char == '!'
    +      @trailing_punctuation = last_char
    +      @link_text.chop!
    +      [@original_scheme, @user, @host, @port, @path, @query, @fragment].compact.last.chop!
    +    else 
    +      @trailing_punctuation = nil
    +    end
    +  end
    +
    +  def scheme
    +    @original_scheme or (@user ? 'mailto' : 'http')
    +  end
    +
    +  def scheme_delimiter
    +    scheme == 'mailto' ? ':' : '://'
    +  end
    +
    +  def user_delimiter
    +     '@' unless @user.nil?
    +  end
    +
    +  def port_delimiter
    +     ':' unless @port.nil?
    +  end
    +
    +  def query_delimiter
    +     '?' unless @query.nil?
    +  end
    +
    +  def uri
    +    [scheme, scheme_delimiter, user, user_delimiter, host, port_delimiter, port, path, 
    +      query_delimiter, query].compact.join
    +  end
    +
    +end
    +
    +# uri with mandatory scheme but less restrictive hostname, like
    +# http://localhost:2500/blah.html
    +class LocalURIChunk < URIChunk
    +
    +  unless defined? LocalURIChunk::LOCAL_URI_REGEXP
    +    # hostname can be just a simple word like 'localhost'
    +    ANY_HOSTNAME = "(?:#{DOMLABEL}\\.)*#{TOPLABEL}\\.?"
    +    
    +    # The basic URI expression as a string
    +    # Scheme and hostname are mandatory
    +    LOCAL_URI =
    +        "(?:(#{SCHEME})://)+" +       # Mandatory scheme://     (\1)
    +        "(?:(#{USERINFO})@)?" +       # Optional userinfo@      (\2)
    +        "(#{ANY_HOSTNAME})" +         # Mandatory hostname      (\3)
    +        "(?::(#{PORT}))?" +           # Optional :port          (\4)
    +        "(#{ABS_PATH})?"  +           # Optional absolute path  (\5)
    +        "(?:\\?(#{QUERY}))?" +        # Optional ?query         (\6)
    +        "(?:\\#(#{FRAGMENT}))?" +     # Optional #fragment      (\7)
    +        '(?=\.?(?:\s|\)|\z))'         # ends only with optional dot + space or ")" 
    +                                      # or end of the string
    +  
    +    LOCAL_URI_REGEXP = Regexp.new(SUSPICIOUS_PRECEDING_CHARACTER + LOCAL_URI, Regexp::EXTENDED, 'N')
    +  end
    +
    +  def LocalURIChunk.pattern
    +    LOCAL_URI_REGEXP
    +  end
    +
    +end
    diff --git a/lib/chunks/wiki.rb b/lib/chunks/wiki.rb
    new file mode 100644
    index 00000000..617e6da5
    --- /dev/null
    +++ b/lib/chunks/wiki.rb
    @@ -0,0 +1,143 @@
    +require 'wiki_words'
    +require 'chunks/chunk'
    +require 'chunks/wiki'
    +require 'cgi'
    +
    +# Contains all the methods for finding and replacing wiki related links.
    +module WikiChunk
    +  include Chunk
    +
    +  # A wiki reference is the top-level class for anything that refers to
    +  # another wiki page.
    +  class WikiReference < Chunk::Abstract
    +
    +    # Name of the referenced page
    +    attr_reader :page_name
    +    
    +    # the referenced page
    +    def refpage
    +      @content.web.page(@page_name)
    +    end
    +  
    +  end
    +
    +  # A wiki link is the top-level class for links that refers to
    +  # another wiki page.
    +  class WikiLink < WikiReference
    + 
    +    attr_reader :link_text, :link_type
    +
    +    def initialize(match_data, content)
    +      super
    +      @link_type = :show
    +    end
    +
    +    def self.apply_to(content)
    +      content.gsub!( self.pattern ) do |matched_text|
    +        chunk = self.new($~, content)
    +        if chunk.textile_url?
    +          # do not substitute
    +          matched_text
    +        else
    +          content.add_chunk(chunk)
    +          chunk.mask
    +        end
    +      end
    +    end
    +
    +    def textile_url?
    +      not @textile_link_suffix.nil?
    +    end
    +
    +    # replace any sequence of whitespace characters with a single space
    +    def normalize_whitespace(line)
    +      line.gsub(/\s+/, ' ')
    +    end
    +  
    +  end
    +
    +  # This chunk matches a WikiWord. WikiWords can be escaped
    +  # by prepending a '\'. When this is the case, the +escaped_text+
    +  # method will return the WikiWord instead of the usual +nil+.
    +  # The +page_name+ method returns the matched WikiWord.
    +  class Word < WikiLink
    +
    +    attr_reader :escaped_text
    +    
    +    unless defined? WIKI_WORD
    +      WIKI_WORD = Regexp.new('(":)?(\\\\)?(' + WikiWords::WIKI_WORD_PATTERN + ')\b', 0, "utf-8")
    +    end
    +
    +    def self.pattern
    +      WIKI_WORD
    +    end
    +
    +    def initialize(match_data, content)
    +      super
    +      @textile_link_suffix, @escape, @page_name = match_data[1..3]
    +      if @escape 
    +        @unmask_mode = :escape
    +        @escaped_text = @page_name
    +      else
    +        @escaped_text = nil
    +      end
    +      @link_text = WikiWords.separate(@page_name)
    +      @unmask_text = (@escaped_text || @content.page_link(@page_name, @link_text, @link_type))
    +    end
    +
    +  end
    +
    +  # This chunk handles [[bracketted wiki words]] and 
    +  # [[AliasedWords|aliased wiki words]]. The first part of an
    +  # aliased wiki word must be a WikiWord. If the WikiWord
    +  # is aliased, the +link_text+ field will contain the
    +  # alias, otherwise +link_text+ will contain the entire
    +  # contents within the double brackets.
    +  #
    +  # NOTE: This chunk must be tested before WikiWord since
    +  #       a WikiWords can be a substring of a WikiLink. 
    +  class Link < WikiLink
    +    
    +    unless defined? WIKI_LINK
    +      WIKI_LINK = /(":)?\[\[\s*([^\]\s][^\]]+?)\s*\]\]/
    +      LINK_TYPE_SEPARATION = Regexp.new('^(.+):((file)|(pic))$', 0, 'utf-8')
    +      ALIAS_SEPARATION = Regexp.new('^(.+)\|(.+)$', 0, 'utf-8')
    +    end    
    +        
    +    def self.pattern() WIKI_LINK end
    +
    +    def initialize(match_data, content)
    +      super
    +      @textile_link_suffix = match_data[1]
    +      @link_text = @page_name = normalize_whitespace(match_data[2])
    +      separate_link_type
    +      separate_alias
    +      @unmask_text = @content.page_link(@page_name, @link_text, @link_type)
    +    end
    +
    +    private
    +
    +    # if link wihin the brackets has a form of [[filename:file]] or [[filename:pic]], 
    +    # this means a link to a picture or a file
    +    def separate_link_type
    +      link_type_match = LINK_TYPE_SEPARATION.match(@page_name)
    +      if link_type_match
    +        @link_text = @page_name = link_type_match[1]
    +        @link_type = link_type_match[2..3].compact[0].to_sym
    +      end
    +    end
    +
    +    # link text may be different from page name. this will look like [[actual page|link text]]
    +    def separate_alias
    +      alias_match = ALIAS_SEPARATION.match(@page_name)
    +      if alias_match
    +        @page_name = normalize_whitespace(alias_match[1])
    +        @link_text = alias_match[2]
    +      end
    +      # note that [[filename|link text:file]] is also supported
    +    end  
    +  
    +  end
    +
    +  
    +end
    diff --git a/lib/diff.rb b/lib/diff.rb
    new file mode 100644
    index 00000000..1e3fe298
    --- /dev/null
    +++ b/lib/diff.rb
    @@ -0,0 +1,316 @@
    +module HTMLDiff
    +
    +  Match = Struct.new(:start_in_old, :start_in_new, :size)
    +  class Match
    +    def end_in_old
    +      self.start_in_old + self.size
    +    end
    +    
    +    def end_in_new
    +      self.start_in_new + self.size
    +    end
    +  end
    +  
    +  Operation = Struct.new(:action, :start_in_old, :end_in_old, :start_in_new, :end_in_new)
    +
    +  class DiffBuilder
    +
    +    def initialize(old_version, new_version)
    +      @old_version, @new_version = old_version, new_version
    +      @content = []
    +    end
    +
    +    def build
    +      split_inputs_to_words
    +      index_new_words
    +      operations.each { |op| perform_operation(op) }
    +      return @content.join
    +    end
    +
    +    def split_inputs_to_words
    +      @old_words = convert_html_to_list_of_words(explode(@old_version))
    +      @new_words = convert_html_to_list_of_words(explode(@new_version))
    +    end
    +
    +    def index_new_words
    +      @word_indices = Hash.new { |h, word| h[word] = [] }
    +      @new_words.each_with_index { |word, i| @word_indices[word] << i }
    +    end
    +
    +    def operations
    +      position_in_old = position_in_new = 0
    +      operations = []
    +      
    +      matches = matching_blocks
    +      # an empty match at the end forces the loop below to handle the unmatched tails
    +      # I'm sure it can be done more gracefully, but not at 23:52
    +      matches << Match.new(@old_words.length, @new_words.length, 0)
    +      
    +      matches.each_with_index do |match, i|
    +        match_starts_at_current_position_in_old = (position_in_old == match.start_in_old)
    +        match_starts_at_current_position_in_new = (position_in_new == match.start_in_new)
    +        
    +        action_upto_match_positions = 
    +          case [match_starts_at_current_position_in_old, match_starts_at_current_position_in_new]
    +          when [false, false]
    +            :replace
    +          when [true, false]
    +            :insert
    +          when [false, true]
    +            :delete
    +          else
    +            # this happens if the first few words are same in both versions
    +            :none
    +          end
    +
    +        if action_upto_match_positions != :none
    +          operation_upto_match_positions = 
    +              Operation.new(action_upto_match_positions, 
    +                  position_in_old, match.start_in_old, 
    +                  position_in_new, match.start_in_new)
    +          operations << operation_upto_match_positions
    +        end
    +        if match.size != 0
    +          match_operation = Operation.new(:equal, 
    +              match.start_in_old, match.end_in_old, 
    +              match.start_in_new, match.end_in_new)
    +          operations << match_operation
    +        end
    +
    +        position_in_old = match.end_in_old
    +        position_in_new = match.end_in_new
    +      end
    +      
    +      operations
    +    end
    +
    +    def matching_blocks
    +      matching_blocks = []
    +      recursively_find_matching_blocks(0, @old_words.size, 0, @new_words.size, matching_blocks)
    +      matching_blocks
    +    end
    +
    +    def recursively_find_matching_blocks(start_in_old, end_in_old, start_in_new, end_in_new, matching_blocks)
    +      match = find_match(start_in_old, end_in_old, start_in_new, end_in_new)
    +      if match
    +        if start_in_old < match.start_in_old and start_in_new < match.start_in_new
    +          recursively_find_matching_blocks(
    +              start_in_old, match.start_in_old, start_in_new, match.start_in_new, matching_blocks) 
    +        end
    +        matching_blocks << match
    +        if match.end_in_old < end_in_old and match.end_in_new < end_in_new
    +          recursively_find_matching_blocks(
    +              match.end_in_old, end_in_old, match.end_in_new, end_in_new, matching_blocks)
    +        end
    +      end
    +    end
    +
    +    def find_match(start_in_old, end_in_old, start_in_new, end_in_new)
    +
    +      best_match_in_old = start_in_old
    +      best_match_in_new = start_in_new
    +      best_match_size = 0
    +      
    +      match_length_at = Hash.new { |h, index| h[index] = 0 }
    +      
    +      start_in_old.upto(end_in_old - 1) do |index_in_old|
    +
    +        new_match_length_at = Hash.new { |h, index| h[index] = 0 }
    +
    +        @word_indices[@old_words[index_in_old]].each do |index_in_new|
    +          next  if index_in_new < start_in_new
    +          break if index_in_new >= end_in_new
    +
    +          new_match_length = match_length_at[index_in_new - 1] + 1
    +          new_match_length_at[index_in_new] = new_match_length
    +
    +          if new_match_length > best_match_size
    +            best_match_in_old = index_in_old - new_match_length + 1
    +            best_match_in_new = index_in_new - new_match_length + 1
    +            best_match_size = new_match_length
    +          end
    +        end
    +        match_length_at = new_match_length_at
    +      end
    +
    +#      best_match_in_old, best_match_in_new, best_match_size = add_matching_words_left(
    +#          best_match_in_old, best_match_in_new, best_match_size, start_in_old, start_in_new)
    +#      best_match_in_old, best_match_in_new, match_size = add_matching_words_right(
    +#          best_match_in_old, best_match_in_new, best_match_size, end_in_old, end_in_new)
    +
    +      return (best_match_size != 0 ? Match.new(best_match_in_old, best_match_in_new, best_match_size) : nil)
    +    end
    +
    +    def add_matching_words_left(match_in_old, match_in_new, match_size, start_in_old, start_in_new)
    +      while match_in_old > start_in_old and 
    +            match_in_new > start_in_new and 
    +            @old_words[match_in_old - 1] == @new_words[match_in_new - 1]
    +        match_in_old -= 1
    +        match_in_new -= 1
    +        match_size += 1
    +      end
    +      [match_in_old, match_in_new, match_size]
    +    end
    +
    +    def add_matching_words_right(match_in_old, match_in_new, match_size, end_in_old, end_in_new)
    +      while match_in_old + match_size < end_in_old and 
    +            match_in_new + match_size < end_in_new and
    +            @old_words[match_in_old + match_size] == @new_words[match_in_new + match_size]
    +        match_size += 1
    +      end
    +      [match_in_old, match_in_new, match_size]
    +    end
    +    
    +    VALID_METHODS = [:replace, :insert, :delete, :equal]
    +
    +    def perform_operation(operation)
    +      @operation = operation
    +      self.send operation.action, operation
    +    end
    +
    +    def replace(operation)
    +      delete(operation, 'diffmod')
    +      insert(operation, 'diffmod')
    +    end
    +    
    +    def insert(operation, tagclass = 'diffins')
    +      insert_tag('ins', tagclass, @new_words[operation.start_in_new...operation.end_in_new])
    +    end
    +    
    +    def delete(operation, tagclass = 'diffdel')
    +       insert_tag('del', tagclass, @old_words[operation.start_in_old...operation.end_in_old])
    +    end
    +    
    +    def equal(operation)
    +      # no tags to insert, simply copy the matching words from one of the versions
    +      @content += @new_words[operation.start_in_new...operation.end_in_new]
    +    end
    +  
    +    def opening_tag?(item)
    +      item =~ %r!^\s*<[^>]+>\s*$!
    +    end
    +
    +    def closing_tag?(item)
    +      item =~ %r!^\s*]+>\s*$!
    +    end
    +
    +    def tag?(item)
    +      opening_tag?(item) or closing_tag?(item)
    +    end
    +
    +    def extract_consecutive_words(words, &condition)
    +      index_of_first_tag = nil
    +      words.each_with_index do |word, i| 
    +        if !condition.call(word)
    +          index_of_first_tag = i
    +          break
    +        end
    +      end
    +      if index_of_first_tag
    +        return words.slice!(0...index_of_first_tag)
    +      else
    +        return words.slice!(0..words.length)
    +      end
    +    end
    +
    +    # This method encloses words within a specified tag (ins or del), and adds this into @content, 
    +    # with a twist: if there are words contain tags, it actually creates multiple ins or del, 
    +    # so that they don't include any ins or del. This handles cases like
    +    # old: '

    a

    ' + # new: '

    ab

    c' + # diff result: '

    ab

    c

    ' + # this still doesn't guarantee valid HTML (hint: think about diffing a text containing ins or + # del tags), but handles correctly more cases than the earlier version. + # + # P.S.: Spare a thought for people who write HTML browsers. They live in this ... every day. + + def insert_tag(tagname, cssclass, words) + loop do + break if words.empty? + non_tags = extract_consecutive_words(words) { |word| not tag?(word) } + @content << wrap_text(non_tags.join, tagname, cssclass) unless non_tags.empty? + + break if words.empty? + @content += extract_consecutive_words(words) { |word| tag?(word) } + end + end + + def wrap_text(text, tagname, cssclass) + %(<#{tagname} class="#{cssclass}">#{text}) + end + + def explode(sequence) + sequence.is_a?(String) ? sequence.split(//) : sequence + end + + def end_of_tag?(char) + char == '>' + end + + def start_of_tag?(char) + char == '<' + end + + def whitespace?(char) + char =~ /\s/ + end + + def convert_html_to_list_of_words(x, use_brackets = false) + mode = :char + current_word = '' + words = [] + + explode(x).each do |char| + case mode + when :tag + if end_of_tag? char + current_word << (use_brackets ? ']' : '>') + words << current_word + current_word = '' + if whitespace?(char) + mode = :whitespace + else + mode = :char + end + else + current_word << char + end + when :char + if start_of_tag? char + words << current_word unless current_word.empty? + current_word = (use_brackets ? '[' : '<') + mode = :tag + elsif /\s/.match char + words << current_word unless current_word.empty? + current_word = char + mode = :whitespace + else + current_word << char + end + when :whitespace + if start_of_tag? char + words << current_word unless current_word.empty? + current_word = (use_brackets ? '[' : '<') + mode = :tag + elsif /\s/.match char + current_word << char + else + words << current_word unless current_word.empty? + current_word = char + mode = :char + end + else + raise "Unknown mode #{mode.inspect}" + end + end + words << current_word unless current_word.empty? + words + end + + end # of class Diff Builder + + def diff(a, b) + DiffBuilder.new(a, b).build + end + +end diff --git a/lib/instiki_errors.rb b/lib/instiki_errors.rb new file mode 100644 index 00000000..0737ab46 --- /dev/null +++ b/lib/instiki_errors.rb @@ -0,0 +1,15 @@ +# Model methods that want to rollback transactions gracefully +# (i.e, returning the user back to the form from which the request was posted) +# should raise Instiki::ValidationError. +# +# E.g. if a model object does +# raise "Foo: '#{foo}' is not equal to Bar: '#{bar}'" if (foo != bar) +# +# then the operation is not committed; Rails returns the user to the page +# where s/he was entering foo and bar, and the error message will be displayed +# on the page + +module Instiki + class ValidationError < StandardError + end +end \ No newline at end of file diff --git a/lib/native/win32/sqlite3.dll b/lib/native/win32/sqlite3.dll new file mode 100644 index 00000000..98c50c4d Binary files /dev/null and b/lib/native/win32/sqlite3.dll differ diff --git a/lib/page_renderer.rb b/lib/page_renderer.rb new file mode 100644 index 00000000..af64602c --- /dev/null +++ b/lib/page_renderer.rb @@ -0,0 +1,134 @@ +require 'diff' +# Temporary class containing all rendering stuff from a Revision +# I want to shift all rendering loguc to the controller eventually + +class PageRenderer + + include HTMLDiff + + def self.setup_url_generator(url_generator) + @@url_generator = url_generator + end + + def self.teardown_url_generator + @@url_generator = nil + end + + attr_reader :revision + + def initialize(revision = nil) + self.revision = revision + end + + def revision=(r) + @revision = r + @display_content = @display_published = @wiki_words_cache = @wiki_includes_cache = + @wiki_references_cache = nil + end + + def display_content(update_references = false) + @display_content ||= render(:update_references => update_references) + end + + def display_content_for_export + render :mode => :export + end + + def display_published + @display_published ||= render(:mode => :publish) + end + + def display_diff + previous_revision = @revision.page.previous_revision(@revision) + if previous_revision + rendered_previous_revision = WikiContent.new(previous_revision, @@url_generator).render! + diff(rendered_previous_revision, display_content) + else + display_content + end + end + + # Returns an array of all the WikiIncludes present in the content of this revision. + def wiki_includes + unless @wiki_includes_cache + chunks = display_content.find_chunks(Include) + @wiki_includes_cache = chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq + end + @wiki_includes_cache + end + + # Returns an array of all the WikiReferences present in the content of this revision. + def wiki_references + unless @wiki_references_cache + chunks = display_content.find_chunks(WikiChunk::WikiReference) + @wiki_references_cache = chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq + end + @wiki_references_cache + end + + # Returns an array of all the WikiWords present in the content of this revision. + def wiki_words + @wiki_words_cache ||= find_wiki_words(display_content) + end + + def find_wiki_words(rendering_result) + wiki_links = rendering_result.find_chunks(WikiChunk::WikiLink) + # Exclude backslash-escaped wiki words, such as \WikiWord, as well as links to files + # and pictures, such as [[foo.txt:file]] or [[foo.jpg:pic]] + wiki_links.delete_if { |link| link.escaped? or [:pic, :file].include?(link.link_type) } + # convert to the list of unique page names + wiki_links.map { |link| ( link.page_name ) }.uniq + end + + # Returns an array of all the WikiWords present in the content of this revision. + # that already exists as a page in the web. + def existing_pages + wiki_words.select { |wiki_word| @revision.page.web.page(wiki_word) } + end + + # Returns an array of all the WikiWords present in the content of this revision + # that *doesn't* already exists as a page in the web. + def unexisting_pages + wiki_words - existing_pages + end + + private + + def render(options = {}) + rendering_result = WikiContent.new(@revision, @@url_generator, options).render! + update_references(rendering_result) if options[:update_references] + rendering_result + end + + def update_references(rendering_result) + WikiReference.delete_all ['page_id = ?', @revision.page_id] + + references = @revision.page.wiki_references + + wiki_words = find_wiki_words(rendering_result) + # TODO it may be desirable to save links to files and pictures as WikiReference objects + # present version doesn't do it + + wiki_words.each do |referenced_name| + # Links to self are always considered linked + if referenced_name == @revision.page.name + link_type = WikiReference::LINKED_PAGE + else + link_type = WikiReference.link_type(@revision.page.web, referenced_name) + end + references.create :referenced_name => referenced_name, :link_type => link_type + end + + include_chunks = rendering_result.find_chunks(Include) + includes = include_chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq + includes.each do |included_page_name| + references.create :referenced_name => included_page_name, + :link_type => WikiReference::INCLUDED_PAGE + end + + categories = rendering_result.find_chunks(Category).map { |cat| cat.list }.flatten + categories.each do |category| + references.create :referenced_name => category, :link_type => WikiReference::CATEGORY + end + end +end diff --git a/lib/rdocsupport.rb b/lib/rdocsupport.rb new file mode 100644 index 00000000..0aaf842f --- /dev/null +++ b/lib/rdocsupport.rb @@ -0,0 +1,152 @@ +begin + require "rdoc/markup/simple_markup" + require 'rdoc/markup/simple_markup/to_html' +rescue LoadError + # use old version if available + require 'markup/simple_markup' + require 'markup/simple_markup/to_html' +end + +module RDocSupport + +# A simple +rdoc+ markup class which recognizes some additional +# formatting commands suitable for Wiki use. +class RDocMarkup < SM::SimpleMarkup + def initialize + super() + + pre = '(?:\\s|^|\\\\)' + + # links of the form + # [[ description with spaces]] + add_special(/((\\)?\[\[\S+?\s+.+?\]\])/,:TIDYLINK) + + # and external references + add_special(/((\\)?(link:|anchor:|http:|mailto:|ftp:|img:|www\.)\S+\w\/?)/, + :HYPERLINK) + + #
    + add_special(%r{(#{pre}
    )}, :BR) + + # and
    ...
    + add_html("center", :CENTER) + end + + def convert(text, handler) + super.sub(/^

    \n/, '').sub(/<\/p>$/, '') + end +end + +# Handle special hyperlinking requirments for RDoc formatted +# entries. Requires RDoc + +class HyperLinkHtml < SM::ToHtml + + # Initialize the HyperLinkHtml object. + # [path] location of the node + # [site] object representing the whole site (typically of class + # +Site+) + def initialize + super() + add_tag(:CENTER, "

    ", "
    ") + end + + # handle
    + def handle_special_BR(special) + return "<br/>" if special.text[0,1] == '\\' + special.text + end + + # We're invoked with a potential external hyperlink. + # [mailto:] just gets inserted. + # [http:] links are checked to see if they + # reference an image. If so, that image gets inserted + # using an tag. Otherwise a conventional + # is used. + # [img:] insert a tag + # [link:] used to insert arbitrary references + # [anchor:] used to create an anchor + def handle_special_HYPERLINK(special) + text = special.text.strip + return text[1..-1] if text[0,1] == '\\' + url = special.text.strip + if url =~ /([A-Za-z]+):(.*)/ + type = $1 + path = $2 + else + type = "http" + path = url + url = "http://#{url}" + end + + case type + when "http" + if url =~ /\.(gif|png|jpg|jpeg|bmp)$/ + "" + else + "#{url.sub(%r{^\w+:/*}, '')}" + end + when "img" + "" + when "link" + "#{path}" + when "anchor" + "" + else + "#{url.sub(%r{^\w+:/*}, '')}" + end + end + + # Here's a hyperlink where the label is different to the URL + # [[url label that may contain spaces]] + # + + def handle_special_TIDYLINK(special) + text = special.text.strip + return text[1..-1] if text[0,1] == '\\' + unless text =~ /\[\[(\S+?)\s+(.+?)\]\]/ + return text + end + url = $1 + label = $2 + label = RDocFormatter.new(label).to_html + label = label.split.select{|x| x =~ /\S/}. + map{|x| x.chomp}.join(' ') + + case url + when /link:(\S+)/ + return %{#{label}} + when /img:(\S+)/ + return %{#{label}} + when /rubytalk:(\S+)/ + return %{#{label}} + when /rubygarden:(\S+)/ + return %{#{label}} + when /c2:(\S+)/ + return %{#{label}} + when /isbn:(\S+)/ + return %{#{label}} + end + + unless url =~ /\w+?:/ + url = "http://#{url}" + end + + "#{label}" + end +end + +class RDocFormatter + def initialize(text) + @text = text + end + + def to_html + markup = RDocMarkup.new + h = HyperLinkHtml.new + markup.convert(@text, h) + end +end + +end \ No newline at end of file diff --git a/lib/redcloth.rb b/lib/redcloth.rb new file mode 100644 index 00000000..1228af6e --- /dev/null +++ b/lib/redcloth.rb @@ -0,0 +1,1130 @@ +# vim:ts=4:sw=4: +# = RedCloth - Textile and Markdown Hybrid for Ruby +# +# Homepage:: http://whytheluckystiff.net/ruby/redcloth/ +# Author:: why the lucky stiff (http://whytheluckystiff.net/) +# Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.) +# License:: BSD +# +# (see http://hobix.com/textile/ for a Textile Reference.) +# +# Based on (and also inspired by) both: +# +# PyTextile: http://diveintomark.org/projects/textile/textile.py.txt +# Textism for PHP: http://www.textism.com/tools/textile/ +# +# + +# = RedCloth +# +# RedCloth is a Ruby library for converting Textile and/or Markdown +# into HTML. You can use either format, intermingled or separately. +# You can also extend RedCloth to honor your own custom text stylings. +# +# RedCloth users are encouraged to use Textile if they are generating +# HTML and to use Markdown if others will be viewing the plain text. +# +# == What is Textile? +# +# Textile is a simple formatting style for text +# documents, loosely based on some HTML conventions. +# +# == Sample Textile Text +# +# h2. This is a title +# +# h3. This is a subhead +# +# This is a bit of paragraph. +# +# bq. This is a blockquote. +# +# = Writing Textile +# +# A Textile document consists of paragraphs. Paragraphs +# can be specially formatted by adding a small instruction +# to the beginning of the paragraph. +# +# h[n]. Header of size [n]. +# bq. Blockquote. +# # Numeric list. +# * Bulleted list. +# +# == Quick Phrase Modifiers +# +# Quick phrase modifiers are also included, to allow formatting +# of small portions of text within a paragraph. +# +# \_emphasis\_ +# \_\_italicized\_\_ +# \*strong\* +# \*\*bold\*\* +# ??citation?? +# -deleted text- +# +inserted text+ +# ^superscript^ +# ~subscript~ +# @code@ +# %(classname)span% +# +# ==notextile== (leave text alone) +# +# == Links +# +# To make a hypertext link, put the link text in "quotation +# marks" followed immediately by a colon and the URL of the link. +# +# Optional: text in (parentheses) following the link text, +# but before the closing quotation mark, will become a Title +# attribute for the link, visible as a tool tip when a cursor is above it. +# +# Example: +# +# "This is a link (This is a title) ":http://www.textism.com +# +# Will become: +# +# This is a link +# +# == Images +# +# To insert an image, put the URL for the image inside exclamation marks. +# +# Optional: text that immediately follows the URL in (parentheses) will +# be used as the Alt text for the image. Images on the web should always +# have descriptive Alt text for the benefit of readers using non-graphical +# browsers. +# +# Optional: place a colon followed by a URL immediately after the +# closing ! to make the image into a link. +# +# Example: +# +# !http://www.textism.com/common/textist.gif(Textist)! +# +# Will become: +# +# Textist +# +# With a link: +# +# !/common/textist.gif(Textist)!:http://textism.com +# +# Will become: +# +# Textist +# +# == Defining Acronyms +# +# HTML allows authors to define acronyms via the tag. The definition appears as a +# tool tip when a cursor hovers over the acronym. A crucial aid to clear writing, +# this should be used at least once for each acronym in documents where they appear. +# +# To quickly define an acronym in Textile, place the full text in (parentheses) +# immediately following the acronym. +# +# Example: +# +# ACLU(American Civil Liberties Union) +# +# Will become: +# +# ACLU +# +# == Adding Tables +# +# In Textile, simple tables can be added by seperating each column by +# a pipe. +# +# |a|simple|table|row| +# |And|Another|table|row| +# +# Attributes are defined by style definitions in parentheses. +# +# table(border:1px solid black). +# (background:#ddd;color:red). |{}| | | | +# +# == Using RedCloth +# +# RedCloth is simply an extension of the String class, which can handle +# Textile formatting. Use it like a String and output HTML with its +# RedCloth#to_html method. +# +# doc = RedCloth.new " +# +# h2. Test document +# +# Just a simple test." +# +# puts doc.to_html +# +# By default, RedCloth uses both Textile and Markdown formatting, with +# Textile formatting taking precedence. If you want to turn off Markdown +# formatting, to boost speed and limit the processor: +# +# class RedCloth::Textile.new( str ) + +class RedCloth < String + + VERSION = '3.0.4' + DEFAULT_RULES = [:textile, :markdown] + + # + # Two accessor for setting security restrictions. + # + # This is a nice thing if you're using RedCloth for + # formatting in public places (e.g. Wikis) where you + # don't want users to abuse HTML for bad things. + # + # If +:filter_html+ is set, HTML which wasn't + # created by the Textile processor will be escaped. + # + # If +:filter_styles+ is set, it will also disable + # the style markup specifier. ('{color: red}') + # + attr_accessor :filter_html, :filter_styles + + # + # Accessor for toggling hard breaks. + # + # If +:hard_breaks+ is set, single newlines will + # be converted to HTML break tags. This is the + # default behavior for traditional RedCloth. + # + attr_accessor :hard_breaks + + # Accessor for toggling lite mode. + # + # In lite mode, block-level rules are ignored. This means + # that tables, paragraphs, lists, and such aren't available. + # Only the inline markup for bold, italics, entities and so on. + # + # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] ) + # r.to_html + # #=> "And then? She fell!" + # + attr_accessor :lite_mode + + # + # Accessor for toggling span caps. + # + # Textile places `span' tags around capitalized + # words by default, but this wreaks havoc on Wikis. + # If +:no_span_caps+ is set, this will be + # suppressed. + # + attr_accessor :no_span_caps + + # + # Establishes the markup predence. Available rules include: + # + # == Textile Rules + # + # The following textile rules can be set individually. Or add the complete + # set of rules with the single :textile rule, which supplies the rule set in + # the following precedence: + # + # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/) + # block_textile_table:: Textile table block structures + # block_textile_lists:: Textile list structures + # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.) + # inline_textile_image:: Textile inline images + # inline_textile_link:: Textile inline links + # inline_textile_span:: Textile inline spans + # glyphs_textile:: Textile entities (such as em-dashes and smart quotes) + # + # == Markdown + # + # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/) + # block_markdown_setext:: Markdown setext headers + # block_markdown_atx:: Markdown atx headers + # block_markdown_rule:: Markdown horizontal rules + # block_markdown_bq:: Markdown blockquotes + # block_markdown_lists:: Markdown lists + # inline_markdown_link:: Markdown links + attr_accessor :rules + + # Returns a new RedCloth object, based on _string_ and + # enforcing all the included _restrictions_. + # + # r = RedCloth.new( "h1. A bold man", [:filter_html] ) + # r.to_html + # #=>"

    A <b>bold</b> man

    " + # + def initialize( string, restrictions = [] ) + restrictions.each { |r| method( "#{ r }=" ).call( true ) } + super( string ) + end + + # + # Generates HTML from the Textile contents. + # + # r = RedCloth.new( "And then? She *fell*!" ) + # r.to_html( true ) + # #=>"And then? She fell!" + # + def to_html( *rules ) + rules = DEFAULT_RULES if rules.empty? + # make our working copy + text = self.dup + + @urlrefs = {} + @shelf = [] + textile_rules = [:refs_textile, :block_textile_table, :block_textile_lists, + :block_textile_prefix, :inline_textile_image, :inline_textile_link, + :inline_textile_code, :inline_textile_span, :glyphs_textile] + markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule, + :block_markdown_bq, :block_markdown_lists, + :inline_markdown_reflink, :inline_markdown_link] + @rules = rules.collect do |rule| + case rule + when :markdown + markdown_rules + when :textile + textile_rules + else + rule + end + end.flatten + + # standard clean up + incoming_entities text + clean_white_space text + + # start processor + @pre_list = [] + rip_offtags text + no_textile text + hard_break text + unless @lite_mode + refs text + blocks text + end + inline text + smooth_offtags text + + retrieve text + + text.gsub!( /<\/?notextile>/, '' ) + text.gsub!( /x%x%/, '&' ) + clean_html text if filter_html + text.strip! + text + + end + + ####### + private + ####### + # + # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents. + # (from PyTextile) + # + TEXTILE_TAGS = + + [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230], + [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249], + [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217], + [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732], + [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]]. + + collect! do |a, b| + [a.chr, ( b.zero? and "" or "&#{ b };" )] + end + + # + # Regular expressions to convert to HTML. + # + A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/ + A_VLGN = /[\-^~]/ + C_CLAS = '(?:\([^)]+\))' + C_LNGE = '(?:\[[^\]]+\])' + C_STYL = '(?:\{[^}]+\})' + S_CSPN = '(?:\\\\\d+)' + S_RSPN = '(?:/\d+)' + A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)" + S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)" + C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)" + # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' ) + PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' ) + PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' ) + PUNCT_Q = Regexp::quote( '*-_+^~%' ) + HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)' + + # Text markup tags, don't conflict with block tags + SIMPLE_HTML_TAGS = [ + 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code', + 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br', + 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo' + ] + + QTAGS = [ + ['**', 'b'], + ['*', 'strong'], + ['??', 'cite', :limit], + ['-', 'del', :limit], + ['__', 'i'], + ['_', 'em', :limit], + ['%', 'span', :limit], + ['+', 'ins', :limit], + ['^', 'sup'], + ['~', 'sub'] + ] + QTAGS.collect! do |rc, ht, rtype| + rcq = Regexp::quote rc + re = + case rtype + when :limit + /(\W) + (#{rcq}) + (#{C}) + (?::(\S+?))? + (\S.*?\S|\S) + #{rcq} + (?=\W)/x + else + /(#{rcq}) + (#{C}) + (?::(\S+))? + (\S.*?\S|\S) + #{rcq}/xm + end + [rc, ht, re, rtype] + end + + # Elements to handle + GLYPHS = [ + # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1’\2' ], # single closing + [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1’' ], # single closing + [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '’' ], # single closing + [ /\'/, '‘' ], # single opening + [ //, '>' ], # greater-than + # [ /([^\s\[{(])?"(\s|:|$)/, '\1”\2' ], # double closing + [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1”' ], # double closing + [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '”' ], # double closing + [ /"/, '“' ], # double opening + [ /\b( )?\.{3}/, '\1…' ], # ellipsis + [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '\1' ], # 3+ uppercase acronym + [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^\2\3', :no_span_caps ], # 3+ uppercase caps + [ /(\.\s)?\s?--\s?/, '\1—' ], # em dash + [ /\s->\s/, ' → ' ], # right arrow + [ /\s-\s/, ' – ' ], # en dash + [ /(\d+) ?x ?(\d+)/, '\1×\2' ], # dimension sign + [ /\b ?[(\[]TM[\])]/i, '™' ], # trademark + [ /\b ?[(\[]R[\])]/i, '®' ], # registered + [ /\b ?[(\[]C[\])]/i, '©' ] # copyright + ] + + H_ALGN_VALS = { + '<' => 'left', + '=' => 'center', + '>' => 'right', + '<>' => 'justify' + } + + V_ALGN_VALS = { + '^' => 'top', + '-' => 'middle', + '~' => 'bottom' + } + + # + # Flexible HTML escaping + # + def htmlesc( str, mode ) + str.gsub!( '&', '&' ) + str.gsub!( '"', '"' ) if mode != :NoQuotes + str.gsub!( "'", ''' ) if mode == :Quotes + str.gsub!( '<', '<') + str.gsub!( '>', '>') + end + + # Search and replace for Textile glyphs (quotes, dashes, other symbols) + def pgl( text ) + GLYPHS.each do |re, resub, tog| + next if tog and method( tog ).call + text.gsub! re, resub + end + end + + # Parses Textile attribute lists and builds an HTML attribute string + def pba( text_in, element = "" ) + + return '' unless text_in + + style = [] + text = text_in.dup + if element == 'td' + colspan = $1 if text =~ /\\(\d+)/ + rowspan = $1 if text =~ /\/(\d+)/ + style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN + end + + style << "#{ $1 };" if not filter_styles and + text.sub!( /\{([^}]*)\}/, '' ) + + lang = $1 if + text.sub!( /\[([^)]+?)\]/, '' ) + + cls = $1 if + text.sub!( /\(([^()]+?)\)/, '' ) + + style << "padding-left:#{ $1.length }em;" if + text.sub!( /([(]+)/, '' ) + + style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' ) + + style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN + + cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/ + + atts = '' + atts << " style=\"#{ style.join }\"" unless style.empty? + atts << " class=\"#{ cls }\"" unless cls.to_s.empty? + atts << " lang=\"#{ lang }\"" if lang + atts << " id=\"#{ id }\"" if id + atts << " colspan=\"#{ colspan }\"" if colspan + atts << " rowspan=\"#{ rowspan }\"" if rowspan + + atts + end + + TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m + + # Parses a Textile table block, building HTML from the result. + def block_textile_table( text ) + text.gsub!( TABLE_RE ) do |matches| + + tatts, fullrow = $~[1..2] + tatts = pba( tatts, 'table' ) + tatts = shelve( tatts ) if tatts + rows = [] + + fullrow. + split( /\|$/m ). + delete_if { |x| x.empty? }. + each do |row| + + ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m + + cells = [] + row.split( '|' ).each do |cell| + ctyp = 'd' + ctyp = 'h' if cell =~ /^_/ + + catts = '' + catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/ + + unless cell.strip.empty? + catts = shelve( catts ) if catts + cells << "\t\t\t#{ cell }" + end + end + ratts = shelve( ratts ) if ratts + rows << "\t\t\n#{ cells.join( "\n" ) }\n\t\t" + end + "\t\n#{ rows.join( "\n" ) }\n\t\n\n" + end + end + + LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m + LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m + + # Parses Textile lists and generates HTML + def block_textile_lists( text ) + text.gsub!( LISTS_RE ) do |match| + lines = match.split( /\n/ ) + last_line = -1 + depth = [] + lines.each_with_index do |line, line_id| + if line =~ LISTS_CONTENT_RE + tl,atts,content = $~[1..3] + if depth.last + if depth.last.length > tl.length + (depth.length - 1).downto(0) do |i| + break if depth[i].length == tl.length + lines[line_id - 1] << "\n\t\n\t" + depth.pop + end + end + if depth.last and depth.last.length == tl.length + lines[line_id - 1] << '' + end + end + unless depth.last == tl + depth << tl + atts = pba( atts ) + atts = shelve( atts ) if atts + lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t
  • #{ content }" + else + lines[line_id] = "\t\t
  • #{ content }" + end + last_line = line_id + + else + last_line = line_id + end + if line_id - last_line > 1 or line_id == lines.length - 1 + depth.delete_if do |v| + lines[last_line] << "
  • \n\t" + end + end + end + lines.join( "\n" ) + end + end + + CODE_RE = /(\W) + @ + (?:\|(\w+?)\|)? + (.+?) + @ + (?=\W)/x + + def inline_textile_code( text ) + text.gsub!( CODE_RE ) do |m| + before,lang,code,after = $~[1..4] + lang = " lang=\"#{ lang }\"" if lang + rip_offtags( "#{ before }#{ code }
    #{ after }" ) + end + end + + def lT( text ) + text =~ /\#$/ ? 'o' : 'u' + end + + def hard_break( text ) + text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1
    " ) if hard_breaks + end + + BLOCKS_GROUP_RE = /\n{2,}(?! )/m + + def blocks( text, deep_code = false ) + text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk| + plain = blk !~ /\A[#*> ]/ + + # skip blocks that are complex HTML + if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1 + blk + else + # search for indentation levels + blk.strip! + if blk.empty? + blk + else + code_blk = nil + blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk| + flush_left iblk + blocks iblk, plain + iblk.gsub( /^(\S)/, "\t\\1" ) + if plain + code_blk = iblk; "" + else + iblk + end + end + + block_applied = 0 + @rules.each do |rule_name| + block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) ) + end + if block_applied.zero? + if deep_code + blk = "\t
    #{ blk }
    " + else + blk = "\t

    #{ blk }

    " + end + end + # hard_break blk + blk + "\n#{ code_blk }" + end + end + + end.join( "\n\n" ) ) + end + + def textile_bq( tag, atts, cite, content ) + cite, cite_title = check_refs( cite ) + cite = " cite=\"#{ cite }\"" if cite + atts = shelve( atts ) if atts + "\t\n\t\t#{ content }

    \n\t" + end + + def textile_p( tag, atts, cite, content ) + atts = shelve( atts ) if atts + "\t<#{ tag }#{ atts }>#{ content }" + end + + alias textile_h1 textile_p + alias textile_h2 textile_p + alias textile_h3 textile_p + alias textile_h4 textile_p + alias textile_h5 textile_p + alias textile_h6 textile_p + + def textile_fn_( tag, num, atts, cite, content ) + atts << " id=\"fn#{ num }\"" + content = "#{ num } #{ content }" + atts = shelve( atts ) if atts + "\t#{ content }

    " + end + + BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m + + def block_textile_prefix( text ) + if text =~ BLOCK_RE + tag,tagpre,num,atts,cite,content = $~[1..6] + atts = pba( atts ) + + # pass to prefix handler + if respond_to? "textile_#{ tag }", true + text.gsub!( $&, method( "textile_#{ tag }" ).call( tag, atts, cite, content ) ) + elsif respond_to? "textile_#{ tagpre }_", true + text.gsub!( $&, method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content ) ) + end + end + end + + SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m + def block_markdown_setext( text ) + if text =~ SETEXT_RE + tag = if $2 == "="; "h1"; else; "h2"; end + blk, cont = "<#{ tag }>#{ $1 }", $' + blocks cont + text.replace( blk + cont ) + end + end + + ATX_RE = /\A(\#{1,6}) # $1 = string of #'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #'s (not counted) + $/x + def block_markdown_atx( text ) + if text =~ ATX_RE + tag = "h#{ $1.length }" + blk, cont = "<#{ tag }>#{ $2 }\n\n", $' + blocks cont + text.replace( blk + cont ) + end + end + + MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m + + def block_markdown_bq( text ) + text.gsub!( MARKDOWN_BQ_RE ) do |blk| + blk.gsub!( /^ *> ?/, '' ) + flush_left blk + blocks blk + blk.gsub!( /^(\S)/, "\t\\1" ) + "
    \n#{ blk }\n
    \n\n" + end + end + + MARKDOWN_RULE_RE = /^(#{ + ['*', '-', '_'].collect { |ch| '( ?' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' ) + })$/ + + def block_markdown_rule( text ) + text.gsub!( MARKDOWN_RULE_RE ) do |blk| + "
    " + end + end + + # XXX TODO XXX + def block_markdown_lists( text ) + end + + def inline_textile_span( text ) + QTAGS.each do |qtag_rc, ht, qtag_re, rtype| + text.gsub!( qtag_re ) do |m| + + case rtype + when :limit + sta,qtag,atts,cite,content = $~[1..5] + else + qtag,atts,cite,content = $~[1..4] + sta = '' + end + atts = pba( atts ) + atts << " cite=\"#{ cite }\"" if cite + atts = shelve( atts ) if atts + + "#{ sta }<#{ ht }#{ atts }>#{ content }" + + end + end + end + + LINK_RE = / + ([\s\[{(]|[#{PUNCT}])? # $pre + " # start + (#{C}) # $atts + ([^"]+?) # $text + \s? + (?:\(([^)]+?)\)(?="))? # $title + ": + (\S+?) # $url + (\/)? # $slash + ([^\w\/;]*?) # $post + (?=<|\s|$) + /x + + def inline_textile_link( text ) + text.gsub!( LINK_RE ) do |m| + pre,atts,text,title,url,slash,post = $~[1..7] + + url, url_title = check_refs( url ) + title ||= url_title + + atts = pba( atts ) + atts = " href=\"#{ url }#{ slash }\"#{ atts }" + atts << " title=\"#{ title }\"" if title + atts = shelve( atts ) if atts + + "#{ pre }#{ text }#{ post }" + end + end + + MARKDOWN_REFLINK_RE = / + \[([^\[\]]+)\] # $text + [ ]? # opt. space + (?:\n[ ]*)? # one optional newline followed by spaces + \[(.*?)\] # $id + /x + + def inline_markdown_reflink( text ) + text.gsub!( MARKDOWN_REFLINK_RE ) do |m| + text, id = $~[1..2] + + if id.empty? + url, title = check_refs( text ) + else + url, title = check_refs( id ) + end + + atts = " href=\"#{ url }\"" + atts << " title=\"#{ title }\"" if title + atts = shelve( atts ) + + "#{ text }" + end + end + + MARKDOWN_LINK_RE = / + \[([^\[\]]+)\] # $text + \( # open paren + [ \t]* # opt space + ? # $href + [ \t]* # opt space + (?: # whole title + (['"]) # $quote + (.*?) # $title + \3 # matching quote + )? # title is optional + \) + /x + + def inline_markdown_link( text ) + text.gsub!( MARKDOWN_LINK_RE ) do |m| + text, url, quote, title = $~[1..4] + + atts = " href=\"#{ url }\"" + atts << " title=\"#{ title }\"" if title + atts = shelve( atts ) + + "#{ text }" + end + end + + TEXTILE_REFS_RE = /(^ *)\[([^\n]+?)\](#{HYPERLINK})(?=\s|$)/ + MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m + + def refs( text ) + @rules.each do |rule_name| + method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/ + end + end + + def refs_textile( text ) + text.gsub!( TEXTILE_REFS_RE ) do |m| + flag, url = $~[2..3] + @urlrefs[flag.downcase] = [url, nil] + nil + end + end + + def refs_markdown( text ) + text.gsub!( MARKDOWN_REFS_RE ) do |m| + flag, url = $~[2..3] + title = $~[6] + @urlrefs[flag.downcase] = [url, title] + nil + end + end + + def check_refs( text ) + ret = @urlrefs[text.downcase] if text + ret || [text, nil] + end + + IMAGE_RE = / + (

    |.|^) # start of line? + \! # opening + (\<|\=|\>)? # optional alignment atts + (#{C}) # optional style,class atts + (?:\. )? # optional dot-space + ([^\s(!]+?) # presume this is the src + \s? # optional space + (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title + \! # closing + (?::#{ HYPERLINK })? # optional href + /x + + def inline_textile_image( text ) + text.gsub!( IMAGE_RE ) do |m| + stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8] + atts = pba( atts ) + atts = " src=\"#{ url }\"#{ atts }" + atts << " title=\"#{ title }\"" if title + atts << " alt=\"#{ title }\"" + # size = @getimagesize($url); + # if($size) $atts.= " $size[3]"; + + href, alt_title = check_refs( href ) if href + url, url_title = check_refs( url ) + + out = '' + out << "" if href + out << "" + out << "#{ href_a1 }#{ href_a2 }" if href + + if algn + algn = h_align( algn ) + if stln == "

    " + out = "

    #{ out }" + else + out = "#{ stln }

    #{ out }
    " + end + else + out = stln + out + end + + out + end + end + + def shelve( val ) + @shelf << val + " :redsh##{ @shelf.length }:" + end + + def retrieve( text ) + @shelf.each_with_index do |r, i| + text.gsub!( " :redsh##{ i + 1 }:", r ) + end + end + + def incoming_entities( text ) + ## turn any incoming ampersands into a dummy character for now. + ## This uses a negative lookahead for alphanumerics followed by a semicolon, + ## implying an incoming html entity, to be skipped + + text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" ) + end + + def no_textile( text ) + text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/, + '\1\2\3' ) + text.gsub!( /^ *==([^=]+.*?)==/m, + '\1\2\3' ) + end + + def clean_white_space( text ) + # normalize line breaks + text.gsub!( /\r\n/, "\n" ) + text.gsub!( /\r/, "\n" ) + text.gsub!( /\t/, ' ' ) + text.gsub!( /^ +$/, '' ) + text.gsub!( /\n{3,}/, "\n\n" ) + text.gsub!( /"$/, "\" " ) + + # if entire document is indented, flush + # to the left side + flush_left text + end + + def flush_left( text ) + indt = 0 + if text =~ /^ / + while text !~ /^ {#{indt}}\S/ + indt += 1 + end unless text.empty? + if indt.nonzero? + text.gsub!( /^ {#{indt}}/, '' ) + end + end + end + + def footnote_ref( text ) + text.gsub!( /\b\[([0-9]+?)\](\s)?/, + '\1\2' ) + end + + OFFTAGS = /(code|pre|kbd|notextile)/ + OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }|\Z)/mi + OFFTAG_OPEN = /<#{ OFFTAGS }/ + OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/ + HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m + ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m + + def glyphs_textile( text, level = 0 ) + if text !~ HASTAG_MATCH + pgl text + footnote_ref text + else + codepre = 0 + text.gsub!( ALLTAG_MATCH ) do |line| + ## matches are off if we're between ,
     etc.
    +                if $1
    +                    if line =~ OFFTAG_OPEN
    +                        codepre += 1
    +                    elsif line =~ OFFTAG_CLOSE
    +                        codepre -= 1
    +                        codepre = 0 if codepre < 0
    +                    end 
    +                elsif codepre.zero?
    +                    glyphs_textile( line, level + 1 )
    +                else
    +                    htmlesc( line, :NoQuotes )
    +                end
    +                # p [level, codepre, line]
    +
    +                line
    +            end
    +        end
    +    end
    +
    +    def rip_offtags( text )
    +        if text =~ /<.*>/
    +            ## strip and encode 
     content
    +            codepre, used_offtags = 0, {}
    +            text.gsub!( OFFTAG_MATCH ) do |line|
    +                if $3
    +                    offtag, aftertag = $4, $5
    +                    codepre += 1
    +                    used_offtags[offtag] = true
    +                    if codepre - used_offtags.length > 0
    +                        htmlesc( line, :NoQuotes ) unless used_offtags['notextile']
    +                        @pre_list.last << line
    +                        line = ""
    +                    else
    +                        htmlesc( aftertag, :NoQuotes ) if aftertag and not used_offtags['notextile']
    +                        line = ""
    +                        @pre_list << "#{ $3 }#{ aftertag }"
    +                    end
    +                elsif $1 and codepre > 0
    +                    if codepre - used_offtags.length > 0
    +                        htmlesc( line, :NoQuotes ) unless used_offtags['notextile']
    +                        @pre_list.last << line
    +                        line = ""
    +                    end
    +                    codepre -= 1 unless codepre.zero?
    +                    used_offtags = {} if codepre.zero?
    +                end 
    +                line
    +            end
    +        end
    +        text
    +    end
    +
    +    def smooth_offtags( text )
    +        unless @pre_list.empty?
    +            ## replace 
     content
    +            text.gsub!( // ) { @pre_list[$1.to_i] }
    +        end
    +    end
    +
    +    def inline( text ) 
    +        [/^inline_/, /^glyphs_/].each do |meth_re|
    +            @rules.each do |rule_name|
    +                method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
    +            end
    +        end
    +    end
    +
    +    def h_align( text ) 
    +        H_ALGN_VALS[text]
    +    end
    +
    +    def v_align( text ) 
    +        V_ALGN_VALS[text]
    +    end
    +
    +    def textile_popup_help( name, windowW, windowH )
    +        ' ' + name + '
    ' + end + + # HTML cleansing stuff + BASIC_TAGS = { + 'a' => ['href', 'title'], + 'img' => ['src', 'alt', 'title'], + 'br' => [], + 'i' => nil, + 'u' => nil, + 'b' => nil, + 'pre' => nil, + 'kbd' => nil, + 'code' => ['lang'], + 'cite' => nil, + 'strong' => nil, + 'em' => nil, + 'ins' => nil, + 'sup' => nil, + 'sub' => nil, + 'del' => nil, + 'table' => nil, + 'tr' => nil, + 'td' => ['colspan', 'rowspan'], + 'th' => nil, + 'ol' => nil, + 'ul' => nil, + 'li' => nil, + 'p' => nil, + 'h1' => nil, + 'h2' => nil, + 'h3' => nil, + 'h4' => nil, + 'h5' => nil, + 'h6' => nil, + 'blockquote' => ['cite'] + } + + def clean_html( text, tags = BASIC_TAGS ) + text.gsub!( /]*)>/ ) do + raw = $~ + tag = raw[2].downcase + if tags.has_key? tag + pcs = [tag] + tags[tag].each do |prop| + ['"', "'", ''].each do |q| + q2 = ( q != '' ? q : '\s' ) + if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i + attrv = $1 + next if prop == 'src' and attrv =~ %r{^(?!http)\w+:} + pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\"" + break + end + end + end if tags[tag] + "<#{raw[1]}#{pcs.join " "}>" + else + " " + end + end + end +end + diff --git a/lib/redcloth_for_tex.rb b/lib/redcloth_for_tex.rb new file mode 100644 index 00000000..6bfb6a0f --- /dev/null +++ b/lib/redcloth_for_tex.rb @@ -0,0 +1,736 @@ +# This is RedCloth (http://www.whytheluckystiff.net/ruby/redcloth/) +# converted by David Heinemeier Hansson to emit Tex + +class String + # Flexible HTML escaping + def texesc!( mode ) + gsub!( '&', '\\\\&' ) + gsub!( '%', '\%' ) + gsub!( '$', '\$' ) + gsub!( '~', '$\sim$' ) + end +end + + +def table_of_contents(text, pages) + text.gsub( /^([#*]+? .*?)$(?![^#*])/m ) do |match| + lines = match.split( /\n/ ) + last_line = -1 + depth = [] + lines.each_with_index do |line, line_id| + if line =~ /^([#*]+) (.*)$/m + tl,content = $~[1..2] + content.gsub! /[\[\]]/, "" + content.strip! + + if depth.last + if depth.last.length > tl.length + (depth.length - 1).downto(0) do |i| + break if depth[i].length == tl.length + lines[line_id - 1] << "" # "\n\t\\end{#{ lT( depth[i] ) }}\n\t" + depth.pop + end + end + if !depth.last.nil? && !tl.length.nil? && depth.last.length == tl.length + lines[line_id - 1] << '' + end + end + + depth << tl unless depth.last == tl + + subsection_depth = [depth.length - 1, 2].min + + lines[line_id] = "\n\\#{ "sub" * subsection_depth }section{#{ content }}" + lines[line_id] += "\n#{pages[content]}" if pages.keys.include?(content) + + lines[line_id] = "\\pagebreak\n#{lines[line_id]}" if subsection_depth == 0 + + last_line = line_id + + elsif line =~ /^\s+\S/ + last_line = line_id + elsif line_id - last_line < 2 and line =~ /^\S/ + last_line = line_id + end + if line_id - last_line > 1 or line_id == lines.length - 1 + depth.delete_if do |v| + lines[last_line] << "" # "\n\t\\end{#{ lT( v ) }}" + end + end + end + lines.join( "\n" ) + end +end + +class RedClothForTex < String + + VERSION = '2.0.7' + + # + # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents. + # (from PyTextile) + # + TEXTILE_TAGS = + + [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230], + [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249], + [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217], + [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732], + [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]]. + + collect! do |a, b| + [a.chr, ( b.zero? and "" or "&#{ b };" )] + end + + # + # Regular expressions to convert to HTML. + # + A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/ + A_VLGN = /[\-^~]/ + C_CLAS = '(?:\([^)]+\))' + C_LNGE = '(?:\[[^\]]+\])' + C_STYL = '(?:\{[^}]+\})' + S_CSPN = '(?:\\\\\d+)' + S_RSPN = '(?:/\d+)' + A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)" + S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)" + C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)" + # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' ) + PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' ) + HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(\s|$)' + + GLYPHS = [ + # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1’\2' ], # single closing + [ /([^\s\[{(>])\'/, '\1’' ], # single closing + [ /\'(?=\s|s\b|[#{PUNCT}])/, '’' ], # single closing + [ /\'/, '‘' ], # single opening + # [ /([^\s\[{(])?"(\s|:|$)/, '\1”\2' ], # double closing + [ /([^\s\[{(>])"/, '\1”' ], # double closing + [ /"(?=\s|[#{PUNCT}])/, '”' ], # double closing + [ /"/, '“' ], # double opening + [ /\b( )?\.{3}/, '\1…' ], # ellipsis + [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '\1' ], # 3+ uppercase acronym + [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]{2,})([^\2\3' ], # 3+ uppercase caps + [ /(\.\s)?\s?--\s?/, '\1—' ], # em dash + [ /\s->\s/, ' → ' ], # en dash + [ /\s-\s/, ' – ' ], # en dash + [ /(\d+) ?x ?(\d+)/, '\1×\2' ], # dimension sign + [ /\b ?[(\[]TM[\])]/i, '™' ], # trademark + [ /\b ?[(\[]R[\])]/i, '®' ], # registered + [ /\b ?[(\[]C[\])]/i, '©' ] # copyright + ] + + I_ALGN_VALS = { + '<' => 'left', + '=' => 'center', + '>' => 'right' + } + + H_ALGN_VALS = { + '<' => 'left', + '=' => 'center', + '>' => 'right', + '<>' => 'justify' + } + + V_ALGN_VALS = { + '^' => 'top', + '-' => 'middle', + '~' => 'bottom' + } + + QTAGS = [ + ['**', 'bf'], + ['*', 'bf'], + ['??', 'cite'], + ['-', 'del'], + ['__', 'underline'], + ['_', 'em'], + ['%', 'span'], + ['+', 'ins'], + ['^', 'sup'], + ['~', 'sub'] + ] + + def self.available? + if not defined? @@available + begin + @@available = system "pdflatex -version" + rescue Errno::ENOENT + @@available = false + end + end + @@available + end + + # + # Two accessor for setting security restrictions. + # + # This is a nice thing if you're using RedCloth for + # formatting in public places (e.g. Wikis) where you + # don't want users to abuse HTML for bad things. + # + # If +:filter_html+ is set, HTML which wasn't + # created by the Textile processor will be escaped. + # + # If +:filter_styles+ is set, it will also disable + # the style markup specifier. ('{color: red}') + # + attr_accessor :filter_html, :filter_styles + + # + # Accessor for toggling line folding. + # + # If +:fold_lines+ is set, single newlines will + # not be converted to break tags. + # + attr_accessor :fold_lines + + def initialize( string, restrictions = [] ) + restrictions.each { |r| method( "#{ r }=" ).call( true ) } + super( string ) + end + + # + # Generate tex. + # + def to_tex( lite = false ) + + # make our working copy + text = self.dup + + @urlrefs = {} + @shelf = [] + + # incoming_entities text + fix_entities text + clean_white_space text + + get_refs text + + no_textile text + + unless lite + lists text + table text + end + + glyphs text + + unless lite + fold text + block text + end + + retrieve text + encode_entities text + + text.gsub!(/\[\[(.*?)\]\]/, "\\1") + text.gsub!(/_/, "\\_") + text.gsub!( /<\/?notextile>/, '' ) + # text.gsub!( /x%x%/, '&' ) + # text.gsub!( /
    /, "
    \n" ) + text.strip! + text + + end + + def pgl( text ) + GLYPHS.each do |re, resub| + text.gsub! re, resub + end + end + + def pba( text_in, element = "" ) + + return '' unless text_in + + style = [] + text = text_in.dup + if element == 'td' + colspan = $1 if text =~ /\\(\d+)/ + rowspan = $1 if text =~ /\/(\d+)/ + style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN + end + + style << "#{ $1 };" if not @filter_styles and + text.sub!( /\{([^}]*)\}/, '' ) + + lang = $1 if + text.sub!( /\[([^)]+?)\]/, '' ) + + cls = $1 if + text.sub!( /\(([^()]+?)\)/, '' ) + + style << "padding-left:#{ $1.length }em;" if + text.sub!( /([(]+)/, '' ) + + style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' ) + + style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN + + cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/ + + atts = '' + atts << " style=\"#{ style.join }\"" unless style.empty? + atts << " class=\"#{ cls }\"" unless cls.to_s.empty? + atts << " lang=\"#{ lang }\"" if lang + atts << " id=\"#{ id }\"" if id + atts << " colspan=\"#{ colspan }\"" if colspan + atts << " rowspan=\"#{ rowspan }\"" if rowspan + + atts + end + + def table( text ) + text << "\n\n" + text.gsub!( /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)\n\n/m ) do |matches| + + tatts, fullrow = $~[1..2] + tatts = pba( tatts, 'table' ) + rows = [] + + fullrow. + split( /\|$/m ). + delete_if { |x| x.empty? }. + each do |row| + + ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m + + cells = [] + row.split( '|' ).each do |cell| + ctyp = 'd' + ctyp = 'h' if cell =~ /^_/ + + catts = '' + catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. )(.*)/ + + unless cell.strip.empty? + cells << "\t\t\t#{ cell }" + end + end + rows << "\t\t\n#{ cells.join( "\n" ) }\n\t\t" + end + "\t\n#{ rows.join( "\n" ) }\n\t\n\n" + end + end + + def lists( text ) + text.gsub!( /^([#*]+?#{C} .*?)$(?![^#*])/m ) do |match| + lines = match.split( /\n/ ) + last_line = -1 + depth = [] + lines.each_with_index do |line, line_id| + if line =~ /^([#*]+)(#{A}#{C}) (.*)$/m + tl,atts,content = $~[1..3] + if depth.last + if depth.last.length > tl.length + (depth.length - 1).downto(0) do |i| + break if depth[i].length == tl.length + lines[line_id - 1] << "\n\t\\end{#{ lT( depth[i] ) }}\n\t" + depth.pop + end + end + if !depth.last.nil? && !tl.length.nil? && depth.last.length == tl.length + lines[line_id - 1] << '' + end + end + unless depth.last == tl + depth << tl + atts = pba( atts ) + lines[line_id] = "\t\\begin{#{ lT(tl) }}\n\t\\item #{ content }" + else + lines[line_id] = "\t\t\\item #{ content }" + end + last_line = line_id + + elsif line =~ /^\s+\S/ + last_line = line_id + elsif line_id - last_line < 2 and line =~ /^\S/ + last_line = line_id + end + if line_id - last_line > 1 or line_id == lines.length - 1 + depth.delete_if do |v| + lines[last_line] << "\n\t\\end{#{ lT( v ) }}" + end + end + end + lines.join( "\n" ) + end + end + + def lT( text ) + text =~ /\#$/ ? 'enumerate' : 'itemize' + end + + def fold( text ) + text.gsub!( /(.+)\n(?![#*\s|])/, "\\1\\\\\\\\" ) + # text.gsub!( /(.+)\n(?![#*\s|])/, "\\1#{ @fold_lines ? ' ' : '
    ' }" ) + end + + def block( text ) + pre = false + find = ['bq','h[1-6]','fn\d+'] + + regexp_cue = [] + + lines = text.split( /\n/ ) + [' '] + new_text = + lines.collect do |line| + pre = true if line =~ /<(pre|notextile)>/i + find.each do |tag| + line.gsub!( /^(#{ tag })(#{A}#{C})\.(?::(\S+))? (.*)$/ ) do |m| + tag,atts,cite,content = $~[1..4] + + atts = pba( atts ) + + if tag =~ /fn(\d+)/ + # tag = 'p'; + # atts << " id=\"fn#{ $1 }\"" + regexp_cue << [ /footnote\{#{$1}}/, "footnote{#{content}}" ] + content = "" + end + + if tag =~ /h([1-6])/ + section_type = "sub" * [$1.to_i - 1, 2].min + start = "\t\\#{section_type}section*{" + tend = "}" + end + + if tag == "bq" + cite = check_refs( cite ) + cite = " cite=\"#{ cite }\"" if cite + start = "\t\\begin{quotation}\n\\noindent {\\em "; + tend = "}\n\t\\end{quotation}"; + end + + "#{ start }#{ content }#{ tend }" + end unless pre + end + + #line.gsub!( /^(?!\t|<\/?pre|<\/?notextile|<\/?code|$| )(.*)/, "\t

    \\1

    " ) + + #line.gsub!( "
    ", "\n" ) if pre + # pre = false if line =~ /<\/(pre|notextile)>/i + + line + end.join( "\n" ) + text.replace( new_text ) + regexp_cue.each { |pair| text.gsub!(pair.first, pair.last) } + end + + def span( text ) + QTAGS.each do |tt, ht| + ttr = Regexp::quote( tt ) + text.gsub!( + + /(^|\s|\>|[#{PUNCT}{(\[]) + #{ttr} + (#{C}) + (?::(\S+?))? + ([^\s#{ttr}]+?(?:[^\n]|\n(?!\n))*?) + ([#{PUNCT}]*?) + #{ttr} + (?=[\])}]|[#{PUNCT}]+?|<|\s|$)/xm + + ) do |m| + + start,atts,cite,content,tend = $~[1..5] + atts = pba( atts ) + atts << " cite=\"#{ cite }\"" if cite + + "#{ start }{\\#{ ht } #{ content }#{ tend }}" + + end + end + end + + def links( text ) + text.gsub!( / + ([\s\[{(]|[#{PUNCT}])? # $pre + " # start + (#{C}) # $atts + ([^"]+?) # $text + \s? + (?:\(([^)]+?)\)(?="))? # $title + ": + (\S+?) # $url + (\/)? # $slash + ([^\w\/;]*?) # $post + (?=\s|$) + /x ) do |m| + pre,atts,text,title,url,slash,post = $~[1..7] + + url.gsub!(/(\\)(.)/, '\2') + url = check_refs( url ) + + atts = pba( atts ) + atts << " title=\"#{ title }\"" if title + atts = shelve( atts ) if atts + + "#{ pre }\\textit{#{ text }} \\footnote{\\texttt{\\textless #{ url }#{ slash }" + + "\\textgreater}#{ post }}" + end + end + + def get_refs( text ) + text.gsub!( /(^|\s)\[(.+?)\]((?:http:\/\/|javascript:|ftp:\/\/|\/)\S+?)(?=\s|$)/ ) do |m| + flag, url = $~[1..2] + @urlrefs[flag] = url + end + end + + def check_refs( text ) + @urlrefs[text] || text + end + + def image( text ) + text.gsub!( / + \! # opening + (\<|\=|\>)? # optional alignment atts + (#{C}) # optional style,class atts + (?:\. )? # optional dot-space + ([^\s(!]+?) # presume this is the src + \s? # optional space + (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title + \! # closing + (?::#{ HYPERLINK })? # optional href + /x ) do |m| + algn,atts,url,title,href,href_a1,href_a2 = $~[1..7] + atts = pba( atts ) + atts << " align=\"#{ i_align( algn ) }\"" if algn + atts << " title=\"#{ title }\"" if title + atts << " alt=\"#{ title }\"" + # size = @getimagesize($url); + # if($size) $atts.= " $size[3]"; + + href = check_refs( href ) if href + url = check_refs( url ) + + out = '' + out << "" if href + out << "" + out << "#{ href_a1 }#{ href_a2 }" if href + + out + end + end + + def code( text ) + text.gsub!( / + (?:^|([\s\(\[{])) # 1 open bracket? + @ # opening + (?:\|(\w+?)\|)? # 2 language + (\S(?:[^\n]|\n(?!\n))*?) # 3 code + @ # closing + (?:$|([\]})])| + (?=[#{PUNCT}]{1,2}| + \s)) # 4 closing bracket? + /x ) do |m| + before,lang,code,after = $~[1..4] + lang = " language=\"#{ lang }\"" if lang + "#{ before }#{ code }
    #{ after }" + end + end + + def shelve( val ) + @shelf << val + " <#{ @shelf.length }>" + end + + def retrieve( text ) + @shelf.each_with_index do |r, i| + text.gsub!( " <#{ i + 1 }>", r ) + end + end + + def incoming_entities( text ) + ## turn any incoming ampersands into a dummy character for now. + ## This uses a negative lookahead for alphanumerics followed by a semicolon, + ## implying an incoming html entity, to be skipped + + text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" ) + end + + def encode_entities( text ) + ## Convert high and low ascii to entities. + # if $-K == "UTF-8" + # encode_high( text ) + # else + text.texesc!( :NoQuotes ) + # end + end + + def fix_entities( text ) + ## de-entify any remaining angle brackets or ampersands + text.gsub!( "\&", "&" ) + text.gsub!( "\%", "%" ) + end + + def clean_white_space( text ) + text.gsub!( /\r\n/, "\n" ) + text.gsub!( /\t/, '' ) + text.gsub!( /\n{3,}/, "\n\n" ) + text.gsub!( /\n *\n/, "\n\n" ) + text.gsub!( /"$/, "\" " ) + end + + def no_textile( text ) + text.gsub!( /(^|\s)==(.*?)==(\s|$)?/, + '\1\2\3' ) + end + + def footnote_ref( text ) + text.gsub!( /\[([0-9]+?)\](\s)?/, + '\footnote{\1}\2') + #'\1\2' ) + end + + def inline( text ) + image text + links text + code text + span text + end + + def glyphs_deep( text ) + codepre = 0 + offtags = /(?:code|pre|kbd|notextile)/ + if text !~ /<.*>/ + # pgl text + footnote_ref text + else + used_offtags = {} + text.gsub!( /(?:[^<].*?(?=<[^\n]*?>|$)|<[^\n]*?>+)/m ) do |line| + tagline = ( line =~ /^<.*>/ ) + + ## matches are off if we're between ,
     etc.
    +          if tagline
    +            if line =~ /<(#{ offtags })>/i
    +              codepre += 1
    +              used_offtags[$1] = true
    +              line.texesc!( :NoQuotes ) if codepre - used_offtags.length > 0
    +            elsif line =~ /<\/(#{ offtags })>/i
    +              line.texesc!( :NoQuotes ) if codepre - used_offtags.length > 0
    +              codepre -= 1 unless codepre.zero?
    +              used_offtags = {} if codepre.zero?
    +            elsif @filter_html or codepre > 0
    +              line.texesc!( :NoQuotes )
    +              ## line.gsub!( /<(\/?#{ offtags })>/, '<\1>' )
    +            end 
    +            ## do htmlspecial if between 
    +          elsif codepre > 0
    +            line.texesc!( :NoQuotes )
    +            ## line.gsub!( /<(\/?#{ offtags })>/, '<\1>' )
    +          elsif not tagline
    +            inline line
    +            glyphs_deep line
    +          end
    +          
    +          line
    +        end
    +      end
    +    end
    +    
    +    def glyphs( text ) 
    +      text.gsub!( /"\z/, "\" " )
    +      ## if no html, do a simple search and replace...
    +      if text !~ /<.*>/
    +        inline text
    +      end
    +      glyphs_deep text
    +    end
    +    
    +    def i_align( text )
    +      I_ALGN_VALS[text]
    +    end
    +    
    +    def h_align( text ) 
    +      H_ALGN_VALS[text]
    +    end
    +    
    +    def v_align( text ) 
    +      V_ALGN_VALS[text]
    +    end
    +    
    +    def encode_high( text )
    +      ## mb_encode_numericentity($text, $cmap, $charset);
    +    end
    +    
    +    def decode_high( text )
    +      ## mb_decode_numericentity($text, $cmap, $charset);
    +    end
    +    
    +    def textile_popup_help( name, helpvar, windowW, windowH )
    +        ' ' + name + '
    ' + end + + CMAP = [ + 160, 255, 0, 0xffff, + 402, 402, 0, 0xffff, + 913, 929, 0, 0xffff, + 931, 937, 0, 0xffff, + 945, 969, 0, 0xffff, + 977, 978, 0, 0xffff, + 982, 982, 0, 0xffff, + 8226, 8226, 0, 0xffff, + 8230, 8230, 0, 0xffff, + 8242, 8243, 0, 0xffff, + 8254, 8254, 0, 0xffff, + 8260, 8260, 0, 0xffff, + 8465, 8465, 0, 0xffff, + 8472, 8472, 0, 0xffff, + 8476, 8476, 0, 0xffff, + 8482, 8482, 0, 0xffff, + 8501, 8501, 0, 0xffff, + 8592, 8596, 0, 0xffff, + 8629, 8629, 0, 0xffff, + 8656, 8660, 0, 0xffff, + 8704, 8704, 0, 0xffff, + 8706, 8707, 0, 0xffff, + 8709, 8709, 0, 0xffff, + 8711, 8713, 0, 0xffff, + 8715, 8715, 0, 0xffff, + 8719, 8719, 0, 0xffff, + 8721, 8722, 0, 0xffff, + 8727, 8727, 0, 0xffff, + 8730, 8730, 0, 0xffff, + 8733, 8734, 0, 0xffff, + 8736, 8736, 0, 0xffff, + 8743, 8747, 0, 0xffff, + 8756, 8756, 0, 0xffff, + 8764, 8764, 0, 0xffff, + 8773, 8773, 0, 0xffff, + 8776, 8776, 0, 0xffff, + 8800, 8801, 0, 0xffff, + 8804, 8805, 0, 0xffff, + 8834, 8836, 0, 0xffff, + 8838, 8839, 0, 0xffff, + 8853, 8853, 0, 0xffff, + 8855, 8855, 0, 0xffff, + 8869, 8869, 0, 0xffff, + 8901, 8901, 0, 0xffff, + 8968, 8971, 0, 0xffff, + 9001, 9002, 0, 0xffff, + 9674, 9674, 0, 0xffff, + 9824, 9824, 0, 0xffff, + 9827, 9827, 0, 0xffff, + 9829, 9830, 0, 0xffff, + 338, 339, 0, 0xffff, + 352, 353, 0, 0xffff, + 376, 376, 0, 0xffff, + 710, 710, 0, 0xffff, + 732, 732, 0, 0xffff, + 8194, 8195, 0, 0xffff, + 8201, 8201, 0, 0xffff, + 8204, 8207, 0, 0xffff, + 8211, 8212, 0, 0xffff, + 8216, 8218, 0, 0xffff, + 8218, 8218, 0, 0xffff, + 8220, 8222, 0, 0xffff, + 8224, 8225, 0, 0xffff, + 8240, 8240, 0, 0xffff, + 8249, 8250, 0, 0xffff, + 8364, 8364, 0, 0xffff + ] + end diff --git a/lib/url_generator.rb b/lib/url_generator.rb new file mode 100644 index 00000000..b5415e63 --- /dev/null +++ b/lib/url_generator.rb @@ -0,0 +1,121 @@ +class AbstractUrlGenerator + + def initialize(controller) + raise 'Controller cannot be nil' if controller.nil? + @controller = controller + end + + # Create a link for the given page (or file) name and link text based + # on the render mode in options and whether the page (file) exists + # in the web. + def make_link(name, web, text = nil, options = {}) + text = CGI.escapeHTML(text || WikiWords.separate(name)) + mode = (options[:mode] || :show).to_sym + link_type = (options[:link_type] || :show).to_sym + + if (link_type == :show) + known_page = web.has_page?(name) + else + known_page = web.has_file?(name) + end + + case link_type + when :show + page_link(mode, name, text, web.address, known_page) + when :file + file_link(mode, name, text, web.address, known_page) + when :pic + pic_link(mode, name, text, web.address, known_page) + else + raise "Unknown link type: #{link_type}" + end + end + +end + +class UrlGenerator < AbstractUrlGenerator + + private + + def file_link(mode, name, text, web_address, known_file) + case mode + when :export + if known_file + %{#{text}} + else + %{#{text}} + end + when :publish + if known_file + href = @controller.url_for :controller => 'file', :web => web_address, :action => 'file', + :id => name + %{#{text}} + else + %{#{text}} + end + else + href = @controller.url_for :controller => 'file', :web => web_address, :action => 'file', + :id => name + if known_file + %{#{text}} + else + %{#{text}?} + end + end + end + + def page_link(mode, name, text, web_address, known_page) + case mode + when :export + if known_page + %{#{text}} + else + %{#{text}} + end + when :publish + if known_page + href = @controller.url_for :controller => 'wiki', :web => web_address, :action => 'published', + :id => name + %{#{text}} + else + %{#{text}} + end + else + if known_page + href = @controller.url_for :controller => 'wiki', :web => web_address, :action => 'show', + :id => name + %{#{text}} + else + href = @controller.url_for :controller => 'wiki', :web => web_address, :action => 'new', + :id => name + %{#{text}?} + end + end + end + + def pic_link(mode, name, text, web_address, known_pic) + case mode + when :export + if known_pic + %{#{text}} + else + %{#{text}} + end + when :publish + if known_pic + %{#{text}} + else + %{#{text}} + end + else + href = @controller.url_for :controller => 'file', :web => web_address, :action => 'file', + :id => name + if known_pic + %{#{text}} + else + %{#{text}?} + end + end + end + +end diff --git a/lib/wiki_content.rb b/lib/wiki_content.rb new file mode 100644 index 00000000..3e348837 --- /dev/null +++ b/lib/wiki_content.rb @@ -0,0 +1,202 @@ +require 'cgi' +require_dependency 'chunks/engines' +require_dependency 'chunks/category' +require_dependency 'chunks/include' +require_dependency 'chunks/wiki' +require_dependency 'chunks/literal' +require_dependency 'chunks/uri' +require_dependency 'chunks/nowiki' + +# Wiki content is just a string that can process itself with a chain of +# actions. The actions can modify wiki content so that certain parts of +# it are protected from being rendered by later actions. +# +# When wiki content is rendered, it can be interrogated to find out +# which chunks were rendered. This means things like categories, wiki +# links, can be determined. +# +# Exactly how wiki content is rendered is determined by a number of +# settings that are optionally passed in to a constructor. The current +# options are: +# * :engine +# => The structural markup engine to use (Textile, Markdown, RDoc) +# * :engine_opts +# => A list of options to pass to the markup engines (safe modes, etc) +# * :pre_engine_actions +# => A list of render actions or chunks to be processed before the +# markup engine is applied. By default this is: +# Category, Include, URIChunk, WikiChunk::Link, WikiChunk::Word +# * :post_engine_actions +# => A list of render actions or chunks to apply after the markup +# engine. By default these are: +# Literal::Pre, Literal::Tags +# * :mode +# => How should the content be rendered? For normal display (show), +# publishing (:publish) or export (:export)? + +module ChunkManager + attr_reader :chunks_by_type, :chunks_by_id, :chunks, :chunk_id + + ACTIVE_CHUNKS = [ NoWiki, Category, WikiChunk::Link, URIChunk, LocalURIChunk, + WikiChunk::Word ] + + HIDE_CHUNKS = [ Literal::Pre, Literal::Tags ] + + MASK_RE = { + ACTIVE_CHUNKS => Chunk::Abstract.mask_re(ACTIVE_CHUNKS), + HIDE_CHUNKS => Chunk::Abstract.mask_re(HIDE_CHUNKS) + } + + def init_chunk_manager + @chunks_by_type = Hash.new + Chunk::Abstract::derivatives.each{|chunk_type| + @chunks_by_type[chunk_type] = Array.new + } + @chunks_by_id = Hash.new + @chunks = [] + @chunk_id = 0 + end + + def add_chunk(c) + @chunks_by_type[c.class] << c + @chunks_by_id[c.object_id] = c + @chunks << c + @chunk_id += 1 + end + + def delete_chunk(c) + @chunks_by_type[c.class].delete(c) + @chunks_by_id.delete(c.object_id) + @chunks.delete(c) + end + + def merge_chunks(other) + other.chunks.each{|c| add_chunk(c)} + end + + def scan_chunkid(text) + text.scan(MASK_RE[ACTIVE_CHUNKS]){|a| yield a[0] } + end + + def find_chunks(chunk_type) + @chunks.select { |chunk| chunk.kind_of?(chunk_type) and chunk.rendered? } + end + +end + +# A simplified version of WikiContent. Useful to avoid recursion problems in +# WikiContent.new +class WikiContentStub < String + + attr_reader :options + include ChunkManager + + def initialize(content, options) + super(content) + @options = options + init_chunk_manager + end + + # Detects the mask strings contained in the text of chunks of type chunk_types + # and yields the corresponding chunk ids + # example: content = "chunk123categorychunk
    chunk456categorychunk
    " + # inside_chunks(Literal::Pre) ==> yield 456 + def inside_chunks(chunk_types) + chunk_types.each{|chunk_type| chunk_type.apply_to(self) } + + chunk_types.each{|chunk_type| @chunks_by_type[chunk_type].each{|hide_chunk| + scan_chunkid(hide_chunk.text){|id| yield id } + } + } + end +end + +class WikiContent < String + + include ChunkManager + + DEFAULT_OPTS = { + :active_chunks => ACTIVE_CHUNKS, + :engine => Engines::Textile, + :engine_opts => [], + :mode => :show + }.freeze + + attr_reader :web, :options, :revision, :not_rendered, :pre_rendered + + # Create a new wiki content string from the given one. + # The options are explained at the top of this file. + def initialize(revision, url_generator, options = {}) + @revision = revision + @url_generator = url_generator + @web = @revision.page.web + + @options = DEFAULT_OPTS.dup.merge(options) + @options[:engine] = Engines::MAP[@web.markup] + @options[:engine_opts] = [:filter_html, :filter_styles] if @web.safe_mode? + @options[:active_chunks] = (ACTIVE_CHUNKS - [WikiChunk::Word] ) if @web.brackets_only? + + @not_rendered = @pre_rendered = nil + + super(@revision.content) + init_chunk_manager + build_chunks + @not_rendered = String.new(self) + end + + # Call @web.page_link using current options. + def page_link(name, text, link_type) + @options[:link_type] = (link_type || :show) + @url_generator.make_link(name, @web, text, @options) + end + + def build_chunks + # create and mask Includes and "active_chunks" chunks + Include.apply_to(self) + @options[:active_chunks].each{|chunk_type| chunk_type.apply_to(self)} + + # Handle hiding contexts like "pre" and "code" etc.. + # The markup (textile, rdoc etc) can produce such contexts with its own syntax. + # To reveal them, we work on a copy of the content. + # The copy is rendered and used to detect the chunks that are inside protecting context + # These chunks are reverted on the original content string. + + copy = WikiContentStub.new(self, @options) + @options[:engine].apply_to(copy) + + copy.inside_chunks(HIDE_CHUNKS) do |id| + @chunks_by_id[id.to_i].revert + end + end + + def pre_render! + unless @pre_rendered + @chunks_by_type[Include].each{|chunk| chunk.unmask } + @pre_rendered = String.new(self) + end + @pre_rendered + end + + def render! + pre_render! + @options[:engine].apply_to(self) + # unmask in one go. $~[1] is the chunk id + gsub!(MASK_RE[ACTIVE_CHUNKS]) do + chunk = @chunks_by_id[$~[1].to_i] + if chunk.nil? + # if we match a chunkmask that existed in the original content string + # just keep it as it is + $~[0] + else + chunk.unmask_text + end + end + self + end + + def page_name + @revision.page.name + end + +end + diff --git a/lib/wiki_words.rb b/lib/wiki_words.rb new file mode 100644 index 00000000..8f2b154f --- /dev/null +++ b/lib/wiki_words.rb @@ -0,0 +1,23 @@ +# Contains all the methods for finding and replacing wiki words +module WikiWords + # In order of appearance: Latin, greek, cyrillian, armenian + I18N_HIGHER_CASE_LETTERS = + "ÀÃ?ÂÃÄÅĀĄĂÆÇĆČĈĊĎÄ?ÈÉÊËĒĘĚĔĖĜĞĠĢĤĦÌÃ?ÃŽÃ?ĪĨĬĮİIJĴĶÅ?ĽĹĻĿÑŃŇŅŊÒÓÔÕÖØŌÅ?ŎŒŔŘŖŚŠŞŜȘŤŢŦȚÙÚÛÜŪŮŰŬŨŲŴÃ?ŶŸŹŽŻ" + + "ΑΒΓΔΕΖΗΘΙΚΛΜÎ?ΞΟΠΡΣΤΥΦΧΨΩ" + + "ΆΈΉΊΌΎÎ?ѠѢѤѦѨѪѬѮѰѲѴѶѸѺѼѾҀҊҌҎÒ?ҒҔҖҘҚҜҞҠҢҤҦҨҪҬҮҰҲҴҶҸҺҼҾÓ?ÓƒÓ…Ó‡Ó‰Ó‹Ó?Ó?ӒӔӖӘӚӜӞӠӢӤӦӨӪӬӮӰӲӴӸЖ" + + "Ô±Ô²Ô³Ô´ÔµÔ¶Ô·Ô¸Ô¹ÔºÔ»Ô¼Ô½Ô¾Ô¿Õ€Õ?Õ‚ÕƒÕ„Õ…Õ†Õ‡ÕˆÕ‰ÕŠÕ‹ÕŒÕ?Õ?Õ?Õ‘Õ’Õ“Õ”Õ•Õ–" + + I18N_LOWER_CASE_LETTERS = + "àáâãäåÄ?ąăæçćÄ?ĉċÄ?đèéêëēęěĕėƒÄ?ğġģĥħìíîïīĩĭįıijĵķĸłľĺļŀñńňņʼnŋòóôõöøÅ?Å‘Å?œŕřŗśšşÅ?șťţŧțùúûüūůűŭũųŵýÿŷžżźÞþßſÃ?ð" + + "άέήίΰαβγδεζηθικλμνξοπÏ?ςστυφχψωϊϋόÏ?ÏŽÎ?" + + "абвгдежзийклмнопрÑ?туфхцчшщъыьÑ?ÑŽÑ?Ñ?ёђѓєѕіїјљћќÑ?ўџѡѣѥѧѩѫѭѯѱѳѵѷѹѻѽѿÒ?Ò‹Ò?Ò?Ò‘Ò“Ò•Ò—Ò™Ò›Ò?ҟҡңҥҧҩҫҭүұҳҵҷҹһҽҿӀӂӄӆӈӊӌӎӑӓӕӗәӛÓ?ÓŸÓ¡Ó£Ó¥Ó§Ó©Ó«Ó­Ó¯Ó±Ó³ÓµÓ¹" + + "Õ¡Õ¢Õ£Õ¤Õ¥Õ¦Õ§Õ¨Õ©ÕªÕ«Õ¬Õ­Õ®Õ¯Õ°Õ±Õ²Õ³Õ´ÕµÕ¶Õ·Õ¸Õ¹ÕºÕ»Õ¼Õ½Õ¾Õ¿Ö€Ö?Ö‚ÖƒÖ„Ö…Ö†Ö‡" + + WIKI_WORD_PATTERN = '[A-Z' + I18N_HIGHER_CASE_LETTERS + '][a-z' + I18N_LOWER_CASE_LETTERS + ']+[A-Z' + I18N_HIGHER_CASE_LETTERS + ']\w+' + CAMEL_CASED_WORD_BORDER = /([a-z#{I18N_LOWER_CASE_LETTERS}])([A-Z#{I18N_HIGHER_CASE_LETTERS}])/u + + def self.separate(wiki_word) + wiki_word.gsub(CAMEL_CASED_WORD_BORDER, '\1 \2') + end + +end diff --git a/natives/osx/desktop_launcher/AppDelegate.h b/natives/osx/desktop_launcher/AppDelegate.h new file mode 100644 index 00000000..c2b8f4f1 --- /dev/null +++ b/natives/osx/desktop_launcher/AppDelegate.h @@ -0,0 +1,18 @@ +/* AppDelegate */ + +#import + +@interface AppDelegate : NSObject +{ + IBOutlet NSMenu* statusMenu; + NSTask* serverCommand; + int processID; + BOOL shouldOpenUntitled; + + NSNetService* service; +} +- (IBAction)about:(id)sender; +- (IBAction)goToHomepage:(id)sender; +- (IBAction)goToInstikiOrg:(id)sender; +- (IBAction)quit:(id)sender; +@end diff --git a/natives/osx/desktop_launcher/AppDelegate.mm b/natives/osx/desktop_launcher/AppDelegate.mm new file mode 100644 index 00000000..be3339cc --- /dev/null +++ b/natives/osx/desktop_launcher/AppDelegate.mm @@ -0,0 +1,109 @@ +#include +#include +#import "AppDelegate.h" + +int launch_ruby (char const* cmd) +{ + int pId, parentID = getpid(); + if((pId = fork()) == 0) // child + { + NSLog(@"set child (%d) to pgrp %d", getpid(), parentID); + setpgrp(0, parentID); + system(cmd); + return 0; + } + else // parent + { + NSLog(@"started child process: %d", pId); + return pId; + } +} + +@implementation AppDelegate + +- (NSString*)storageDirectory +{ + NSString* dir = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/Application Support/Instiki"]; + [[NSFileManager defaultManager] createDirectoryAtPath:dir attributes:nil]; + return dir; +} + +- (void)awakeFromNib +{ + setpgrp(0, getpid()); + + if([[[[NSBundle mainBundle] infoDictionary] objectForKey:@"LSUIElement"] isEqualToString:@"1"]) + { + NSStatusBar* bar = [NSStatusBar systemStatusBar]; + NSStatusItem* item = [[bar statusItemWithLength:NSVariableStatusItemLength] retain]; + [item setTitle:@"Wiki"]; + [item setHighlightMode:YES]; + [item setMenu:statusMenu]; + } + + NSBundle* bundle = [NSBundle bundleForClass:[self class]]; + NSString* ruby = [bundle pathForResource:@"ruby" ofType:nil]; + NSString* script = [[bundle resourcePath] stringByAppendingPathComponent:@"rb_src/instiki.rb"]; + if(ruby && script) + { + NSString* cmd = [NSString stringWithFormat: + @"%@ -I '%@' -I '%@' '%@' -s --storage='%@'", + ruby, + [[bundle resourcePath] stringByAppendingPathComponent:@"lib/ruby/1.8"], + [[bundle resourcePath] stringByAppendingPathComponent:@"lib/ruby/1.8/powerpc-darwin"], + script, + [self storageDirectory] + ]; + NSLog(@"starting %@", cmd); + processID = launch_ruby([cmd UTF8String]); + } + + /* public the service using rendezvous */ + service = [[NSNetService alloc] + initWithDomain:@"" // default domain + type:@"_http._tcp." + name:[NSString stringWithFormat:@"%@'s Instiki", NSFullUserName()] + port:2500]; + [service publish]; +} + +- (void)applicationWillTerminate:(NSNotification*)aNotification +{ + [service stop]; + [service release]; + + kill(0, SIGTERM); +} + +- (IBAction)about:(id)sender +{ + [NSApp activateIgnoringOtherApps:YES]; + [NSApp orderFrontStandardAboutPanel:self]; +} + +- (IBAction)goToHomepage:(id)sender +{ + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://localhost:2500/"]]; +} + +- (IBAction)goToInstikiOrg:(id)sender +{ + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.instiki.org/"]]; +} + +- (BOOL)applicationShouldOpenUntitledFile:(NSApplication*)sender +{ + return shouldOpenUntitled ?: (shouldOpenUntitled = YES, NO); +} + +- (BOOL)applicationOpenUntitledFile:(NSApplication*)theApplication +{ + return [self goToHomepage:self], YES; +} + +- (IBAction)quit:(id)sender +{ + [NSApp terminate:self]; +} + +@end diff --git a/natives/osx/desktop_launcher/Credits.html b/natives/osx/desktop_launcher/Credits.html new file mode 100644 index 00000000..99ff9627 --- /dev/null +++ b/natives/osx/desktop_launcher/Credits.html @@ -0,0 +1,16 @@ +
    +
    Engineering:
    +
    Some people
    + +
    Human Interface Design:
    +
    Some other people
    + +
    Testing:
    +
    Hopefully not nobody
    + +
    Documentation:
    +
    Whoever
    + +
    With special thanks to:
    +
    Mom
    +
    \ No newline at end of file diff --git a/natives/osx/desktop_launcher/English.lproj/InfoPlist.strings b/natives/osx/desktop_launcher/English.lproj/InfoPlist.strings new file mode 100644 index 00000000..9ead5aa9 Binary files /dev/null and b/natives/osx/desktop_launcher/English.lproj/InfoPlist.strings differ diff --git a/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/classes.nib b/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/classes.nib new file mode 100644 index 00000000..60ddec12 --- /dev/null +++ b/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/classes.nib @@ -0,0 +1,13 @@ +{ + IBClasses = ( + { + ACTIONS = {about = id; goToHomepage = id; goToInstikiOrg = id; quit = id; }; + CLASS = AppDelegate; + LANGUAGE = ObjC; + OUTLETS = {statusMenu = NSMenu; }; + SUPERCLASS = NSObject; + }, + {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; } + ); + IBVersion = 1; +} \ No newline at end of file diff --git a/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/info.nib b/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/info.nib new file mode 100644 index 00000000..3ef7d504 --- /dev/null +++ b/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/info.nib @@ -0,0 +1,24 @@ + + + + + IBDocumentLocation + 109 6 356 240 0 0 1440 878 + IBEditorPositions + + 206 + 112 300 116 87 0 0 1440 878 + 29 + 241 316 70 44 0 0 1440 878 + + IBFramework Version + 349.0 + IBOpenObjects + + 206 + 29 + + IBSystem Version + 7H63 + + diff --git a/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/objects.nib b/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/objects.nib new file mode 100644 index 00000000..e78d3042 Binary files /dev/null and b/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/objects.nib differ diff --git a/natives/osx/desktop_launcher/Info.plist b/natives/osx/desktop_launcher/Info.plist new file mode 100644 index 00000000..c2c9f3bf --- /dev/null +++ b/natives/osx/desktop_launcher/Info.plist @@ -0,0 +1,13 @@ +{ + CFBundleDevelopmentRegion = English; + CFBundleExecutable = Instiki; + CFBundleIconFile = ""; + CFBundleIdentifier = "com.nextangle.instiki"; + CFBundleInfoDictionaryVersion = "6.0"; + CFBundlePackageType = APPL; + CFBundleSignature = WIKI; + CFBundleVersion = "0.9.0"; + LSUIElement = 1; + NSMainNibFile = MainMenu; + NSPrincipalClass = NSApplication; +} \ No newline at end of file diff --git a/natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj b/natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj new file mode 100644 index 00000000..f71cb7bc --- /dev/null +++ b/natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj @@ -0,0 +1,592 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 39; + objects = { + 080E96DDFE201D6D7F000001 = { + children = ( + 174B2765065CE31400ED6208, + 174B2766065CE31400ED6208, + ); + isa = PBXGroup; + name = Classes; + refType = 4; + sourceTree = ""; + }; + 089C165CFE840E0CC02AAC07 = { + children = ( + 089C165DFE840E0CC02AAC07, + ); + isa = PBXVariantGroup; + name = InfoPlist.strings; + refType = 4; + sourceTree = ""; + }; + 089C165DFE840E0CC02AAC07 = { + fileEncoding = 10; + isa = PBXFileReference; + lastKnownFileType = text.plist.strings; + name = English; + path = English.lproj/InfoPlist.strings; + refType = 4; + sourceTree = ""; + }; +//080 +//081 +//082 +//083 +//084 +//100 +//101 +//102 +//103 +//104 + 1058C7A0FEA54F0111CA2CBB = { + children = ( + 1058C7A1FEA54F0111CA2CBB, + ); + isa = PBXGroup; + name = "Linked Frameworks"; + refType = 4; + sourceTree = ""; + }; + 1058C7A1FEA54F0111CA2CBB = { + fallbackIsa = PBXFileReference; + isa = PBXFrameworkReference; + lastKnownFileType = wrapper.framework; + name = Cocoa.framework; + path = /System/Library/Frameworks/Cocoa.framework; + refType = 0; + sourceTree = ""; + }; + 1058C7A2FEA54F0111CA2CBB = { + children = ( + 29B97325FDCFA39411CA2CEA, + 29B97324FDCFA39411CA2CEA, + ); + isa = PBXGroup; + name = "Other Frameworks"; + refType = 4; + sourceTree = ""; + }; +//100 +//101 +//102 +//103 +//104 +//170 +//171 +//172 +//173 +//174 + 174B2765065CE31400ED6208 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = sourcecode.cpp.objcpp; + path = AppDelegate.mm; + refType = 4; + sourceTree = ""; + }; + 174B2766065CE31400ED6208 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = sourcecode.c.h; + path = AppDelegate.h; + refType = 4; + sourceTree = ""; + }; + 174B2767065CE31400ED6208 = { + fileRef = 174B2765065CE31400ED6208; + isa = PBXBuildFile; + settings = { + }; + }; + 174B2768065CE31400ED6208 = { + fileRef = 174B2766065CE31400ED6208; + isa = PBXBuildFile; + settings = { + }; + }; + 17BF6FD9067536EB003F37D6 = { + children = ( + 63B86D2F0673A5D300807E13, + 63B86D1A0673A5B200807E13, + 63B86D100673A58400807E13, + ); + isa = PBXGroup; + name = "Instiki Source"; + refType = 4; + sourceTree = ""; + }; + 17C1C5CD065D3A3C003526E7 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = text.html; + path = Credits.html; + refType = 4; + sourceTree = ""; + }; + 17C1C5CE065D3A3C003526E7 = { + fileRef = 17C1C5CD065D3A3C003526E7; + isa = PBXBuildFile; + settings = { + }; + }; + 17C1C6E2065D458D003526E7 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = text.script.sh; + path = MakeDMG.sh; + refType = 4; + sourceTree = ""; + }; + 17F6C11106629574007E0BD0 = { + isa = PBXFileReference; + lastKnownFileType = "compiled.mach-o.executable"; + name = ruby; + path = /usr/local/bin/ruby; + refType = 0; + sourceTree = ""; + }; + 17F6C11206629574007E0BD0 = { + fileRef = 17F6C11106629574007E0BD0; + isa = PBXBuildFile; + settings = { + }; + }; + 17F6C113066295D0007E0BD0 = { + isa = PBXFileReference; + lastKnownFileType = folder; + name = ruby; + path = /usr/local/lib/ruby; + refType = 0; + sourceTree = ""; + }; + 17F6C3A90662960F007E0BD0 = { + buildActionMask = 2147483647; + dstPath = lib; + dstSubfolderSpec = 7; + files = ( + 17F6C3CF066296B5007E0BD0, + ); + isa = PBXCopyFilesBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 17F6C3CF066296B5007E0BD0 = { + fileRef = 17F6C113066295D0007E0BD0; + isa = PBXBuildFile; + settings = { + }; + }; + 17F6C3D2066296E4007E0BD0 = { + children = ( + 17F6C11106629574007E0BD0, + 17F6C113066295D0007E0BD0, + ); + isa = PBXGroup; + name = "Ruby 1.8"; + refType = 4; + sourceTree = ""; + }; +//170 +//171 +//172 +//173 +//174 +//190 +//191 +//192 +//193 +//194 + 19C28FACFE9D520D11CA2CBB = { + children = ( + 8D1107320486CEB800E47090, + ); + isa = PBXGroup; + name = Products; + refType = 4; + sourceTree = ""; + }; +//190 +//191 +//192 +//193 +//194 +//290 +//291 +//292 +//293 +//294 + 29B97313FDCFA39411CA2CEA = { + buildSettings = { + }; + buildStyles = ( + 4A9504CCFFE6A4B311CA0CBA, + 4A9504CDFFE6A4B311CA0CBA, + ); + hasScannedForEncodings = 1; + isa = PBXProject; + mainGroup = 29B97314FDCFA39411CA2CEA; + projectDirPath = ""; + targets = ( + 8D1107260486CEB800E47090, + ); + }; + 29B97314FDCFA39411CA2CEA = { + children = ( + 080E96DDFE201D6D7F000001, + 29B97315FDCFA39411CA2CEA, + 29B97317FDCFA39411CA2CEA, + 29B97323FDCFA39411CA2CEA, + 19C28FACFE9D520D11CA2CBB, + 17C1C6E2065D458D003526E7, + ); + isa = PBXGroup; + name = Instiki; + path = ""; + refType = 4; + sourceTree = ""; + }; + 29B97315FDCFA39411CA2CEA = { + children = ( + 32CA4F630368D1EE00C91783, + 29B97316FDCFA39411CA2CEA, + ); + isa = PBXGroup; + name = "Other Sources"; + path = ""; + refType = 4; + sourceTree = ""; + }; + 29B97316FDCFA39411CA2CEA = { + fileEncoding = 30; + isa = PBXFileReference; + lastKnownFileType = sourcecode.cpp.objcpp; + path = main.mm; + refType = 4; + sourceTree = ""; + }; + 29B97317FDCFA39411CA2CEA = { + children = ( + 17BF6FD9067536EB003F37D6, + 17F6C3D2066296E4007E0BD0, + 8D1107310486CEB800E47090, + 089C165CFE840E0CC02AAC07, + 29B97318FDCFA39411CA2CEA, + 17C1C5CD065D3A3C003526E7, + ); + isa = PBXGroup; + name = Resources; + path = ""; + refType = 4; + sourceTree = ""; + }; + 29B97318FDCFA39411CA2CEA = { + children = ( + 29B97319FDCFA39411CA2CEA, + ); + isa = PBXVariantGroup; + name = MainMenu.nib; + path = ""; + refType = 4; + sourceTree = ""; + }; + 29B97319FDCFA39411CA2CEA = { + isa = PBXFileReference; + lastKnownFileType = wrapper.nib; + name = English; + path = English.lproj/MainMenu.nib; + refType = 4; + sourceTree = ""; + }; + 29B97323FDCFA39411CA2CEA = { + children = ( + 1058C7A0FEA54F0111CA2CBB, + 1058C7A2FEA54F0111CA2CBB, + ); + isa = PBXGroup; + name = Frameworks; + path = ""; + refType = 4; + sourceTree = ""; + }; + 29B97324FDCFA39411CA2CEA = { + fallbackIsa = PBXFileReference; + isa = PBXFrameworkReference; + lastKnownFileType = wrapper.framework; + name = AppKit.framework; + path = /System/Library/Frameworks/AppKit.framework; + refType = 0; + sourceTree = ""; + }; + 29B97325FDCFA39411CA2CEA = { + fallbackIsa = PBXFileReference; + isa = PBXFrameworkReference; + lastKnownFileType = wrapper.framework; + name = Foundation.framework; + path = /System/Library/Frameworks/Foundation.framework; + refType = 0; + sourceTree = ""; + }; +//290 +//291 +//292 +//293 +//294 +//320 +//321 +//322 +//323 +//324 + 32CA4F630368D1EE00C91783 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = sourcecode.c.h; + path = Instiki_Prefix.pch; + refType = 4; + sourceTree = ""; + }; +//320 +//321 +//322 +//323 +//324 +//4A0 +//4A1 +//4A2 +//4A3 +//4A4 + 4A9504CCFFE6A4B311CA0CBA = { + buildRules = ( + ); + buildSettings = { + COPY_PHASE_STRIP = NO; + DEBUGGING_SYMBOLS = YES; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + OPTIMIZATION_CFLAGS = "-O0"; + ZERO_LINK = YES; + }; + isa = PBXBuildStyle; + name = Development; + }; + 4A9504CDFFE6A4B311CA0CBA = { + buildRules = ( + ); + buildSettings = { + COPY_PHASE_STRIP = YES; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + ZERO_LINK = NO; + }; + isa = PBXBuildStyle; + name = Deployment; + }; +//4A0 +//4A1 +//4A2 +//4A3 +//4A4 +//630 +//631 +//632 +//633 +//634 + 63B86D0F0673A53100807E13 = { + buildActionMask = 2147483647; + dstPath = rb_src; + dstSubfolderSpec = 7; + files = ( + 63B86D310673A5D600807E13, + 63B86D1C0673A5B600807E13, + 63B86D120673A59100807E13, + ); + isa = PBXCopyFilesBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 63B86D100673A58400807E13 = { + explicitFileType = folder; + fileEncoding = 4; + isa = PBXFileReference; + name = app; + path = /Users/duff/Source/rb_src/instiki/app; + refType = 0; + sourceTree = ""; + }; + 63B86D120673A59100807E13 = { + fileRef = 63B86D100673A58400807E13; + isa = PBXBuildFile; + settings = { + }; + }; + 63B86D1A0673A5B200807E13 = { + explicitFileType = folder; + fileEncoding = 4; + isa = PBXFileReference; + name = libraries; + path = /Users/duff/Source/rb_src/instiki/libraries; + refType = 0; + sourceTree = ""; + }; + 63B86D1C0673A5B600807E13 = { + fileRef = 63B86D1A0673A5B200807E13; + isa = PBXBuildFile; + settings = { + }; + }; + 63B86D2F0673A5D300807E13 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = text.script.ruby; + name = instiki.rb; + path = /Users/duff/Source/rb_src/instiki/instiki.rb; + refType = 0; + sourceTree = ""; + }; + 63B86D310673A5D600807E13 = { + fileRef = 63B86D2F0673A5D300807E13; + isa = PBXBuildFile; + settings = { + }; + }; +//630 +//631 +//632 +//633 +//634 +//8D0 +//8D1 +//8D2 +//8D3 +//8D4 + 8D1107260486CEB800E47090 = { + buildPhases = ( + 8D1107270486CEB800E47090, + 8D1107290486CEB800E47090, + 8D11072C0486CEB800E47090, + 8D11072E0486CEB800E47090, + 17F6C3A90662960F007E0BD0, + 63B86D0F0673A53100807E13, + ); + buildRules = ( + ); + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ""; + GCC_ENABLE_TRIGRAPHS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = Instiki_Prefix.pch; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = NO; + GCC_WARN_FOUR_CHARACTER_CONSTANTS = NO; + GCC_WARN_UNKNOWN_PRAGMAS = NO; + HEADER_SEARCH_PATHS = ""; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Applications"; + LIBRARY_SEARCH_PATHS = ""; + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ""; + PRODUCT_NAME = Instiki; + SECTORDER_FLAGS = ""; + WARNING_CFLAGS = "-Wmost -Wno-four-char-constants -Wno-unknown-pragmas"; + WRAPPER_EXTENSION = app; + }; + dependencies = ( + ); + isa = PBXNativeTarget; + name = Instiki; + productInstallPath = "$(HOME)/Applications"; + productName = Instiki; + productReference = 8D1107320486CEB800E47090; + productType = "com.apple.product-type.application"; + }; + 8D1107270486CEB800E47090 = { + buildActionMask = 2147483647; + files = ( + 8D1107280486CEB800E47090, + 174B2768065CE31400ED6208, + ); + isa = PBXHeadersBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 8D1107280486CEB800E47090 = { + fileRef = 32CA4F630368D1EE00C91783; + isa = PBXBuildFile; + settings = { + }; + }; + 8D1107290486CEB800E47090 = { + buildActionMask = 2147483647; + files = ( + 8D11072A0486CEB800E47090, + 8D11072B0486CEB800E47090, + 17C1C5CE065D3A3C003526E7, + 17F6C11206629574007E0BD0, + ); + isa = PBXResourcesBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 8D11072A0486CEB800E47090 = { + fileRef = 29B97318FDCFA39411CA2CEA; + isa = PBXBuildFile; + settings = { + }; + }; + 8D11072B0486CEB800E47090 = { + fileRef = 089C165CFE840E0CC02AAC07; + isa = PBXBuildFile; + settings = { + }; + }; + 8D11072C0486CEB800E47090 = { + buildActionMask = 2147483647; + files = ( + 8D11072D0486CEB800E47090, + 174B2767065CE31400ED6208, + ); + isa = PBXSourcesBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 8D11072D0486CEB800E47090 = { + fileRef = 29B97316FDCFA39411CA2CEA; + isa = PBXBuildFile; + settings = { + ATTRIBUTES = ( + ); + }; + }; + 8D11072E0486CEB800E47090 = { + buildActionMask = 2147483647; + files = ( + 8D11072F0486CEB800E47090, + ); + isa = PBXFrameworksBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 8D11072F0486CEB800E47090 = { + fileRef = 1058C7A1FEA54F0111CA2CBB; + isa = PBXBuildFile; + settings = { + }; + }; + 8D1107310486CEB800E47090 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = text.plist; + path = Info.plist; + refType = 4; + sourceTree = ""; + }; + 8D1107320486CEB800E47090 = { + explicitFileType = wrapper.application; + includeInIndex = 0; + isa = PBXFileReference; + path = Instiki.app; + refType = 3; + sourceTree = BUILT_PRODUCTS_DIR; + }; + }; + rootObject = 29B97313FDCFA39411CA2CEA; +} diff --git a/natives/osx/desktop_launcher/Instiki_Prefix.pch b/natives/osx/desktop_launcher/Instiki_Prefix.pch new file mode 100644 index 00000000..4212f5ef --- /dev/null +++ b/natives/osx/desktop_launcher/Instiki_Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'Instiki' target in the 'Instiki' project +// + +#ifdef __OBJC__ + #import +#endif diff --git a/natives/osx/desktop_launcher/MakeDMG.sh b/natives/osx/desktop_launcher/MakeDMG.sh new file mode 100644 index 00000000..d1cddeb1 --- /dev/null +++ b/natives/osx/desktop_launcher/MakeDMG.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +hdiutil create -size 12m -fs HFS+ -volname Instiki -ov /tmp/Instiki_12MB.dmg +hdiutil mount /tmp/Instiki_12MB.dmg +# strip ~/ruby/instiki/natives/osx/build/Instiki.app/Contents/MacOS/Instiki +ditto ~/ruby/instiki/natives/osx/desktop_launcher/build/Instiki.app /Volumes/Instiki/Instiki.app +hdiutil unmount /Volumes/Instiki +hdiutil convert -format UDZO -o /tmp/Instiki.dmg /tmp/Instiki_12MB.dmg +hdiutil internet-enable -yes /tmp/Instiki.dmg diff --git a/natives/osx/desktop_launcher/main.mm b/natives/osx/desktop_launcher/main.mm new file mode 100644 index 00000000..0eb41cfe --- /dev/null +++ b/natives/osx/desktop_launcher/main.mm @@ -0,0 +1,14 @@ +// +// main.mm +// Instiki +// +// Created by Allan Odgaard on Thu May 20 2004. +// Copyright (c) 2004 MacroMates. All rights reserved. +// + +#import + +int main (int argc, char const* argv[]) +{ + return NSApplicationMain(argc, argv); +} diff --git a/natives/osx/desktop_launcher/version.plist b/natives/osx/desktop_launcher/version.plist new file mode 100644 index 00000000..a2932018 --- /dev/null +++ b/natives/osx/desktop_launcher/version.plist @@ -0,0 +1,16 @@ + + + + + BuildVersion + 17 + CFBundleShortVersionString + 0.1 + CFBundleVersion + 0.1 + ProjectName + NibPBTemplates + SourceVersion + 1150000 + + diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 00000000..d3c99834 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,40 @@ +# General Apache options +AddHandler fastcgi-script .fcgi +AddHandler cgi-script .cgi +Options +FollowSymLinks +ExecCGI + +# If you don't want Rails to look in certain directories, +# use the following rewrite rules so that Apache won't rewrite certain requests +# +# Example: +# RewriteCond %{REQUEST_URI} ^/notrails.* +# RewriteRule .* - [L] + +# Redirect all requests not available on the filesystem to Rails +# By default the cgi dispatcher is used which is very slow +# +# For better performance replace the dispatcher with the fastcgi one +# +# Example: +# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] +RewriteEngine On + +# If your Rails application is accessed via an Alias directive, +# then you MUST also set the RewriteBase in this htaccess file. +# +# Example: +# Alias /myrailsapp /path/to/myrailsapp/public +# RewriteBase /myrailsapp + +RewriteRule ^$ index.html [QSA] +RewriteRule ^([^.]+)$ $1.html [QSA] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^(.*)$ dispatch.cgi [QSA,L] + +# In case Rails experiences terminal errors +# Instead of displaying this message you can supply a file here which will be rendered instead +# +# Example: +# ErrorDocument 500 /500.html + +ErrorDocument 500 "

    Application error

    Rails application failed to start properly" \ No newline at end of file diff --git a/public/404.html b/public/404.html new file mode 100644 index 00000000..0e184561 --- /dev/null +++ b/public/404.html @@ -0,0 +1,8 @@ + + + +

    File not found

    +

    Change this error message for pages not found in public/404.html

    + + \ No newline at end of file diff --git a/public/500.html b/public/500.html new file mode 100644 index 00000000..a1001a00 --- /dev/null +++ b/public/500.html @@ -0,0 +1,8 @@ + + + +

    Application error (Apache)

    +

    Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html

    + + \ No newline at end of file diff --git a/public/dispatch.cgi b/public/dispatch.cgi new file mode 100755 index 00000000..ce705d36 --- /dev/null +++ b/public/dispatch.cgi @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) + +# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: +# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired +require "dispatcher" + +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) +Dispatcher.dispatch \ No newline at end of file diff --git a/public/dispatch.fcgi b/public/dispatch.fcgi new file mode 100755 index 00000000..664dbbbe --- /dev/null +++ b/public/dispatch.fcgi @@ -0,0 +1,24 @@ +#!/usr/bin/env ruby +# +# You may specify the path to the FastCGI crash log (a log of unhandled +# exceptions which forced the FastCGI instance to exit, great for debugging) +# and the number of requests to process before running garbage collection. +# +# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log +# and the GC period is nil (turned off). A reasonable number of requests +# could range from 10-100 depending on the memory footprint of your app. +# +# Example: +# # Default log path, normal GC behavior. +# RailsFCGIHandler.process! +# +# # Default log path, 50 requests between GC. +# RailsFCGIHandler.process! nil, 50 +# +# # Custom log path, normal GC behavior. +# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log' +# +require File.dirname(__FILE__) + "/../config/environment" +require 'fcgi_handler' + +RailsFCGIHandler.process! diff --git a/public/dispatch.rb b/public/dispatch.rb new file mode 100755 index 00000000..ce705d36 --- /dev/null +++ b/public/dispatch.rb @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) + +# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: +# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired +require "dispatcher" + +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) +Dispatcher.dispatch \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..e6136658 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/images/.images_go_here b/public/images/.images_go_here new file mode 100644 index 00000000..e69de29b diff --git a/public/images/bg_normal.gif b/public/images/bg_normal.gif new file mode 100644 index 00000000..76c0f484 Binary files /dev/null and b/public/images/bg_normal.gif differ diff --git a/public/images/bg_protected.gif b/public/images/bg_protected.gif new file mode 100644 index 00000000..2030f6f0 Binary files /dev/null and b/public/images/bg_protected.gif differ diff --git a/public/javascripts/controls.js b/public/javascripts/controls.js new file mode 100644 index 00000000..a7436bcf --- /dev/null +++ b/public/javascripts/controls.js @@ -0,0 +1,708 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005 Jon Tirsen (http://www.tirsen.com) +// Contributors: +// Richard Livsey +// Rahul Bhargava +// Rob Wills +// +// See scriptaculous.js for full license. + +// Autocompleter.Base handles all the autocompletion functionality +// that's independent of the data source for autocompletion. This +// includes drawing the autocompletion menu, observing keyboard +// and mouse events, and similar. +// +// Specific autocompleters need to provide, at the very least, +// a getUpdatedChoices function that will be invoked every time +// the text inside the monitored textbox changes. This method +// should get the text for which to provide autocompletion by +// invoking this.getToken(), NOT by directly accessing +// this.element.value. This is to allow incremental tokenized +// autocompletion. Specific auto-completion logic (AJAX, etc) +// belongs in getUpdatedChoices. +// +// Tokenized incremental autocompletion is enabled automatically +// when an autocompleter is instantiated with the 'tokens' option +// in the options parameter, e.g.: +// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); +// will incrementally autocomplete with a comma as the token. +// Additionally, ',' in the above example can be replaced with +// a token array, e.g. { tokens: [',', '\n'] } which +// enables autocompletion on multiple tokens. This is most +// useful when one of the tokens is \n (a newline), as it +// allows smart autocompletion after linebreaks. + +var Autocompleter = {} +Autocompleter.Base = function() {}; +Autocompleter.Base.prototype = { + baseInitialize: function(element, update, options) { + this.element = $(element); + this.update = $(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entryCount = 0; + + if (this.setOptions) + this.setOptions(options); + else + this.options = options || {}; + + this.options.paramName = this.options.paramName || this.element.name; + this.options.tokens = this.options.tokens || []; + this.options.frequency = this.options.frequency || 0.4; + this.options.minChars = this.options.minChars || 1; + this.options.onShow = this.options.onShow || + function(element, update){ + if(!update.style.position || update.style.position=='absolute') { + update.style.position = 'absolute'; + Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight}); + } + Effect.Appear(update,{duration:0.15}); + }; + this.options.onHide = this.options.onHide || + function(element, update){ new Effect.Fade(update,{duration:0.15}) }; + + if (typeof(this.options.tokens) == 'string') + this.options.tokens = new Array(this.options.tokens); + + this.observer = null; + + this.element.setAttribute('autocomplete','off'); + + Element.hide(this.update); + + Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); + }, + + show: function() { + if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && (Element.getStyle(this.update, 'position')=='absolute')) { + new Insertion.After(this.update, + ''); + this.iefix = $(this.update.id+'_iefix'); + } + if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); + }, + + fixIEOverlapping: function() { + Position.clone(this.update, this.iefix); + this.iefix.style.zIndex = 1; + this.update.style.zIndex = 2; + Element.show(this.iefix); + }, + + hide: function() { + this.stopIndicator(); + if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); + if(this.iefix) Element.hide(this.iefix); + }, + + startIndicator: function() { + if(this.options.indicator) Element.show(this.options.indicator); + }, + + stopIndicator: function() { + if(this.options.indicator) Element.hide(this.options.indicator); + }, + + onKeyPress: function(event) { + if(this.active) + switch(event.keyCode) { + case Event.KEY_TAB: + case Event.KEY_RETURN: + this.selectEntry(); + Event.stop(event); + case Event.KEY_ESC: + this.hide(); + this.active = false; + Event.stop(event); + return; + case Event.KEY_LEFT: + case Event.KEY_RIGHT: + return; + case Event.KEY_UP: + this.markPrevious(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + case Event.KEY_DOWN: + this.markNext(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) + return; + + this.changed = true; + this.hasFocus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); + }, + + onHover: function(event) { + var element = Event.findElement(event, 'LI'); + if(this.index != element.autocompleteIndex) + { + this.index = element.autocompleteIndex; + this.render(); + } + Event.stop(event); + }, + + onClick: function(event) { + var element = Event.findElement(event, 'LI'); + this.index = element.autocompleteIndex; + this.selectEntry(); + this.hide(); + }, + + onBlur: function(event) { + // needed to make click events working + setTimeout(this.hide.bind(this), 250); + this.hasFocus = false; + this.active = false; + }, + + render: function() { + if(this.entryCount > 0) { + for (var i = 0; i < this.entryCount; i++) + this.index==i ? + Element.addClassName(this.getEntry(i),"selected") : + Element.removeClassName(this.getEntry(i),"selected"); + + if(this.hasFocus) { + this.show(); + this.active = true; + } + } else this.hide(); + }, + + markPrevious: function() { + if(this.index > 0) this.index-- + else this.index = this.entryCount-1; + }, + + markNext: function() { + if(this.index < this.entryCount-1) this.index++ + else this.index = 0; + }, + + getEntry: function(index) { + return this.update.firstChild.childNodes[index]; + }, + + getCurrentEntry: function() { + return this.getEntry(this.index); + }, + + selectEntry: function() { + this.active = false; + this.updateElement(this.getCurrentEntry()); + }, + + updateElement: function(selectedElement) { + if (this.options.updateElement) { + this.options.updateElement(selectedElement); + return; + } + + var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + var lastTokenPos = this.findLastToken(); + if (lastTokenPos != -1) { + var newValue = this.element.value.substr(0, lastTokenPos + 1); + var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/); + if (whitespace) + newValue += whitespace[0]; + this.element.value = newValue + value; + } else { + this.element.value = value; + } + this.element.focus(); + + if (this.options.afterUpdateElement) + this.options.afterUpdateElement(this.element, selectedElement); + }, + + updateChoices: function(choices) { + if(!this.changed && this.hasFocus) { + this.update.innerHTML = choices; + Element.cleanWhitespace(this.update); + Element.cleanWhitespace(this.update.firstChild); + + if(this.update.firstChild && this.update.firstChild.childNodes) { + this.entryCount = + this.update.firstChild.childNodes.length; + for (var i = 0; i < this.entryCount; i++) { + var entry = this.getEntry(i); + entry.autocompleteIndex = i; + this.addObservers(entry); + } + } else { + this.entryCount = 0; + } + + this.stopIndicator(); + + this.index = 0; + this.render(); + } + }, + + addObservers: function(element) { + Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); + Event.observe(element, "click", this.onClick.bindAsEventListener(this)); + }, + + onObserverEvent: function() { + this.changed = false; + if(this.getToken().length>=this.options.minChars) { + this.startIndicator(); + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + }, + + getToken: function() { + var tokenPos = this.findLastToken(); + if (tokenPos != -1) + var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,''); + else + var ret = this.element.value; + + return /\n/.test(ret) ? '' : ret; + }, + + findLastToken: function() { + var lastTokenPos = -1; + + for (var i=0; i lastTokenPos) + lastTokenPos = thisTokenPos; + } + return lastTokenPos; + } +} + +Ajax.Autocompleter = Class.create(); +Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), { + initialize: function(element, update, url, options) { + this.baseInitialize(element, update, options); + this.options.asynchronous = true; + this.options.onComplete = this.onComplete.bind(this); + this.options.defaultParams = this.options.parameters || null; + this.url = url; + }, + + getUpdatedChoices: function() { + entry = encodeURIComponent(this.options.paramName) + '=' + + encodeURIComponent(this.getToken()); + + this.options.parameters = this.options.callback ? + this.options.callback(this.element, entry) : entry; + + if(this.options.defaultParams) + this.options.parameters += '&' + this.options.defaultParams; + + new Ajax.Request(this.url, this.options); + }, + + onComplete: function(request) { + this.updateChoices(request.responseText); + } + +}); + +// The local array autocompleter. Used when you'd prefer to +// inject an array of autocompletion options into the page, rather +// than sending out Ajax queries, which can be quite slow sometimes. +// +// The constructor takes four parameters. The first two are, as usual, +// the id of the monitored textbox, and id of the autocompletion menu. +// The third is the array you want to autocomplete from, and the fourth +// is the options block. +// +// Extra local autocompletion options: +// - choices - How many autocompletion choices to offer +// +// - partialSearch - If false, the autocompleter will match entered +// text only at the beginning of strings in the +// autocomplete array. Defaults to true, which will +// match text at the beginning of any *word* in the +// strings in the autocomplete array. If you want to +// search anywhere in the string, additionally set +// the option fullSearch to true (default: off). +// +// - fullSsearch - Search anywhere in autocomplete array strings. +// +// - partialChars - How many characters to enter before triggering +// a partial match (unlike minChars, which defines +// how many characters are required to do any match +// at all). Defaults to 2. +// +// - ignoreCase - Whether to ignore case when autocompleting. +// Defaults to true. +// +// It's possible to pass in a custom function as the 'selector' +// option, if you prefer to write your own autocompletion logic. +// In that case, the other options above will not apply unless +// you support them. + +Autocompleter.Local = Class.create(); +Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { + initialize: function(element, update, array, options) { + this.baseInitialize(element, update, options); + this.options.array = array; + }, + + getUpdatedChoices: function() { + this.updateChoices(this.options.selector(this)); + }, + + setOptions: function(options) { + this.options = Object.extend({ + choices: 10, + partialSearch: true, + partialChars: 2, + ignoreCase: true, + fullSearch: false, + selector: function(instance) { + var ret = []; // Beginning matches + var partial = []; // Inside matches + var entry = instance.getToken(); + var count = 0; + + for (var i = 0; i < instance.options.array.length && + ret.length < instance.options.choices ; i++) { + + var elem = instance.options.array[i]; + var foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : + elem.indexOf(entry); + + while (foundPos != -1) { + if (foundPos == 0 && elem.length != entry.length) { + ret.push("
  • " + elem.substr(0, entry.length) + "" + + elem.substr(entry.length) + "
  • "); + break; + } else if (entry.length >= instance.options.partialChars && + instance.options.partialSearch && foundPos != -1) { + if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { + partial.push("
  • " + elem.substr(0, foundPos) + "" + + elem.substr(foundPos, entry.length) + "" + elem.substr( + foundPos + entry.length) + "
  • "); + break; + } + } + + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + elem.indexOf(entry, foundPos + 1); + + } + } + if (partial.length) + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) + return "
      " + ret.join('') + "
    "; + } + }, options || {}); + } +}); + +// AJAX in-place editor +// +// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor + +Ajax.InPlaceEditor = Class.create(); +Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99"; +Ajax.InPlaceEditor.prototype = { + initialize: function(element, url, options) { + this.url = url; + this.element = $(element); + + this.options = Object.extend({ + okText: "ok", + cancelText: "cancel", + savingText: "Saving...", + clickToEditText: "Click to edit", + okText: "ok", + rows: 1, + onComplete: function(transport, element) { + new Effect.Highlight(element, {startcolor: this.options.highlightcolor}); + }, + onFailure: function(transport) { + alert("Error communicating with the server: " + transport.responseText.stripTags()); + }, + callback: function(form) { + return Form.serialize(form); + }, + handleLineBreaks: true, + loadingText: 'Loading...', + savingClassName: 'inplaceeditor-saving', + loadingClassName: 'inplaceeditor-loading', + formClassName: 'inplaceeditor-form', + highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, + highlightendcolor: "#FFFFFF", + externalControl: null, + ajaxOptions: {} + }, options || {}); + + if(!this.options.formId && this.element.id) { + this.options.formId = this.element.id + "-inplaceeditor"; + if ($(this.options.formId)) { + // there's already a form with that name, don't specify an id + this.options.formId = null; + } + } + + if (this.options.externalControl) { + this.options.externalControl = $(this.options.externalControl); + } + + this.originalBackground = Element.getStyle(this.element, 'background-color'); + if (!this.originalBackground) { + this.originalBackground = "transparent"; + } + + this.element.title = this.options.clickToEditText; + + this.onclickListener = this.enterEditMode.bindAsEventListener(this); + this.mouseoverListener = this.enterHover.bindAsEventListener(this); + this.mouseoutListener = this.leaveHover.bindAsEventListener(this); + Event.observe(this.element, 'click', this.onclickListener); + Event.observe(this.element, 'mouseover', this.mouseoverListener); + Event.observe(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.observe(this.options.externalControl, 'click', this.onclickListener); + Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + }, + enterEditMode: function() { + if (this.saving) return; + if (this.editing) return; + this.editing = true; + this.onEnterEditMode(); + if (this.options.externalControl) { + Element.hide(this.options.externalControl); + } + Element.hide(this.element); + this.createForm(); + this.element.parentNode.insertBefore(this.form, this.element); + Field.focus(this.editField); + // stop the event to avoid a page refresh in Safari + if (arguments.length > 1) { + Event.stop(arguments[0]); + } + }, + createForm: function() { + this.form = document.createElement("form"); + this.form.id = this.options.formId; + Element.addClassName(this.form, this.options.formClassName) + this.form.onsubmit = this.onSubmit.bind(this); + + this.createEditField(); + + if (this.options.textarea) { + var br = document.createElement("br"); + this.form.appendChild(br); + } + + okButton = document.createElement("input"); + okButton.type = "submit"; + okButton.value = this.options.okText; + this.form.appendChild(okButton); + + cancelLink = document.createElement("a"); + cancelLink.href = "#"; + cancelLink.appendChild(document.createTextNode(this.options.cancelText)); + cancelLink.onclick = this.onclickCancel.bind(this); + this.form.appendChild(cancelLink); + }, + hasHTMLLineBreaks: function(string) { + if (!this.options.handleLineBreaks) return false; + return string.match(/
    /i); + }, + convertHTMLLineBreaks: function(string) { + return string.replace(/
    /gi, "\n").replace(//gi, "\n").replace(/<\/p>/gi, "\n").replace(/

    /gi, ""); + }, + createEditField: function() { + var text; + if(this.options.loadTextURL) { + text = this.options.loadingText; + } else { + text = this.getText(); + } + + if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { + this.options.textarea = false; + var textField = document.createElement("input"); + textField.type = "text"; + textField.name = "value"; + textField.value = text; + textField.style.backgroundColor = this.options.highlightcolor; + var size = this.options.size || this.options.cols || 0; + if (size != 0) textField.size = size; + this.editField = textField; + } else { + this.options.textarea = true; + var textArea = document.createElement("textarea"); + textArea.name = "value"; + textArea.value = this.convertHTMLLineBreaks(text); + textArea.rows = this.options.rows; + textArea.cols = this.options.cols || 40; + this.editField = textArea; + } + + if(this.options.loadTextURL) { + this.loadExternalText(); + } + this.form.appendChild(this.editField); + }, + getText: function() { + return this.element.innerHTML; + }, + loadExternalText: function() { + Element.addClassName(this.form, this.options.loadingClassName); + this.editField.disabled = true; + new Ajax.Request( + this.options.loadTextURL, + Object.extend({ + asynchronous: true, + onComplete: this.onLoadedExternalText.bind(this) + }, this.options.ajaxOptions) + ); + }, + onLoadedExternalText: function(transport) { + Element.removeClassName(this.form, this.options.loadingClassName); + this.editField.disabled = false; + this.editField.value = transport.responseText.stripTags(); + }, + onclickCancel: function() { + this.onComplete(); + this.leaveEditMode(); + return false; + }, + onFailure: function(transport) { + this.options.onFailure(transport); + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + this.oldInnerHTML = null; + } + return false; + }, + onSubmit: function() { + // onLoading resets these so we need to save them away for the Ajax call + var form = this.form; + var value = this.editField.value; + + // do this first, sometimes the ajax call returns before we get a chance to switch on Saving... + // which means this will actually switch on Saving... *after* we've left edit mode causing Saving... + // to be displayed indefinitely + this.onLoading(); + + new Ajax.Updater( + { + success: this.element, + // don't update on failure (this could be an option) + failure: null + }, + this.url, + Object.extend({ + parameters: this.options.callback(form, value), + onComplete: this.onComplete.bind(this), + onFailure: this.onFailure.bind(this) + }, this.options.ajaxOptions) + ); + // stop the event to avoid a page refresh in Safari + if (arguments.length > 1) { + Event.stop(arguments[0]); + } + return false; + }, + onLoading: function() { + this.saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + showSaving: function() { + this.oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + Element.addClassName(this.element, this.options.savingClassName); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + }, + removeForm: function() { + if(this.form) { + if (this.form.parentNode) Element.remove(this.form); + this.form = null; + } + }, + enterHover: function() { + if (this.saving) return; + this.element.style.backgroundColor = this.options.highlightcolor; + if (this.effect) { + this.effect.cancel(); + } + Element.addClassName(this.element, this.options.hoverClassName) + }, + leaveHover: function() { + if (this.options.backgroundColor) { + this.element.style.backgroundColor = this.oldBackground; + } + Element.removeClassName(this.element, this.options.hoverClassName) + if (this.saving) return; + this.effect = new Effect.Highlight(this.element, { + startcolor: this.options.highlightcolor, + endcolor: this.options.highlightendcolor, + restorecolor: this.originalBackground + }); + }, + leaveEditMode: function() { + Element.removeClassName(this.element, this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + if (this.options.externalControl) { + Element.show(this.options.externalControl); + } + this.editing = false; + this.saving = false; + this.oldInnerHTML = null; + this.onLeaveEditMode(); + }, + onComplete: function(transport) { + this.leaveEditMode(); + this.options.onComplete.bind(this)(transport, this.element); + }, + onEnterEditMode: function() {}, + onLeaveEditMode: function() {}, + dispose: function() { + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + } + this.leaveEditMode(); + Event.stopObserving(this.element, 'click', this.onclickListener); + Event.stopObserving(this.element, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.stopObserving(this.options.externalControl, 'click', this.onclickListener); + Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + } +}; \ No newline at end of file diff --git a/public/javascripts/dragdrop.js b/public/javascripts/dragdrop.js new file mode 100644 index 00000000..5445d748 --- /dev/null +++ b/public/javascripts/dragdrop.js @@ -0,0 +1,516 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// +// Element.Class part Copyright (c) 2005 by Rick Olson +// +// See scriptaculous.js for full license. + +/*--------------------------------------------------------------------------*/ + +var Droppables = { + drops: [], + + remove: function(element) { + this.drops = this.drops.reject(function(d) { return d.element==element }); + }, + + add: function(element) { + element = $(element); + var options = Object.extend({ + greedy: true, + hoverclass: null + }, arguments[1] || {}); + + // cache containers + if(options.containment) { + options._containers = []; + var containment = options.containment; + if((typeof containment == 'object') && + (containment.constructor == Array)) { + containment.each( function(c) { options._containers.push($(c)) }); + } else { + options._containers.push($(containment)); + } + } + + Element.makePositioned(element); // fix IE + options.element = element; + + this.drops.push(options); + }, + + isContained: function(element, drop) { + var parentNode = element.parentNode; + return drop._containers.detect(function(c) { return parentNode == c }); + }, + + isAffected: function(pX, pY, element, drop) { + return ( + (drop.element!=element) && + ((!drop._containers) || + this.isContained(element, drop)) && + ((!drop.accept) || + (Element.Class.has_any(element, drop.accept))) && + Position.within(drop.element, pX, pY) ); + }, + + deactivate: function(drop) { + if(drop.hoverclass) + Element.Class.remove(drop.element, drop.hoverclass); + this.last_active = null; + }, + + activate: function(drop) { + if(this.last_active) this.deactivate(this.last_active); + if(drop.hoverclass) + Element.Class.add(drop.element, drop.hoverclass); + this.last_active = drop; + }, + + show: function(event, element) { + if(!this.drops.length) return; + var pX = Event.pointerX(event); + var pY = Event.pointerY(event); + Position.prepare(); + + var i = this.drops.length-1; do { + var drop = this.drops[i]; + if(this.isAffected(pX, pY, element, drop)) { + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + if(drop.greedy) { + this.activate(drop); + return; + } + } + } while (i--); + + if(this.last_active) this.deactivate(this.last_active); + }, + + fire: function(event, element) { + if(!this.last_active) return; + Position.prepare(); + + if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active)) + if (this.last_active.onDrop) + this.last_active.onDrop(element, this.last_active.element, event); + }, + + reset: function() { + if(this.last_active) + this.deactivate(this.last_active); + } +} + +var Draggables = { + observers: [], + addObserver: function(observer) { + this.observers.push(observer); + }, + removeObserver: function(element) { // element instead of obsever fixes mem leaks + this.observers = this.observers.reject( function(o) { return o.element==element }); + }, + notify: function(eventName, draggable) { // 'onStart', 'onEnd' + this.observers.invoke(eventName, draggable); + } +} + +/*--------------------------------------------------------------------------*/ + +var Draggable = Class.create(); +Draggable.prototype = { + initialize: function(element) { + var options = Object.extend({ + handle: false, + starteffect: function(element) { + new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); + }, + reverteffect: function(element, top_offset, left_offset) { + var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; + new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur}); + }, + endeffect: function(element) { + new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); + }, + zindex: 1000, + revert: false + }, arguments[1] || {}); + + this.element = $(element); + if(options.handle && (typeof options.handle == 'string')) + this.handle = Element.Class.childrenWith(this.element, options.handle)[0]; + + if(!this.handle) this.handle = $(options.handle); + if(!this.handle) this.handle = this.element; + + Element.makePositioned(this.element); // fix IE + + this.offsetX = 0; + this.offsetY = 0; + this.originalLeft = this.currentLeft(); + this.originalTop = this.currentTop(); + this.originalX = this.element.offsetLeft; + this.originalY = this.element.offsetTop; + + this.options = options; + + this.active = false; + this.dragging = false; + + this.eventMouseDown = this.startDrag.bindAsEventListener(this); + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.update.bindAsEventListener(this); + this.eventKeypress = this.keyPress.bindAsEventListener(this); + + this.registerEvents(); + }, + destroy: function() { + Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); + this.unregisterEvents(); + }, + registerEvents: function() { + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + Event.observe(document, "keypress", this.eventKeypress); + Event.observe(this.handle, "mousedown", this.eventMouseDown); + }, + unregisterEvents: function() { + //if(!this.active) return; + //Event.stopObserving(document, "mouseup", this.eventMouseUp); + //Event.stopObserving(document, "mousemove", this.eventMouseMove); + //Event.stopObserving(document, "keypress", this.eventKeypress); + }, + currentLeft: function() { + return parseInt(this.element.style.left || '0'); + }, + currentTop: function() { + return parseInt(this.element.style.top || '0') + }, + startDrag: function(event) { + if(Event.isLeftClick(event)) { + + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if(src.tagName && ( + src.tagName=='INPUT' || + src.tagName=='SELECT' || + src.tagName=='BUTTON' || + src.tagName=='TEXTAREA')) return; + + // this.registerEvents(); + this.active = true; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var offsets = Position.cumulativeOffset(this.element); + this.offsetX = (pointer[0] - offsets[0]); + this.offsetY = (pointer[1] - offsets[1]); + Event.stop(event); + } + }, + finishDrag: function(event, success) { + // this.unregisterEvents(); + + this.active = false; + this.dragging = false; + + if(this.options.ghosting) { + Position.relativize(this.element); + Element.remove(this._clone); + this._clone = null; + } + + if(success) Droppables.fire(event, this.element); + Draggables.notify('onEnd', this); + + var revert = this.options.revert; + if(revert && typeof revert == 'function') revert = revert(this.element); + + if(revert && this.options.reverteffect) { + this.options.reverteffect(this.element, + this.currentTop()-this.originalTop, + this.currentLeft()-this.originalLeft); + } else { + this.originalLeft = this.currentLeft(); + this.originalTop = this.currentTop(); + } + + if(this.options.zindex) + this.element.style.zIndex = this.originalZ; + + if(this.options.endeffect) + this.options.endeffect(this.element); + + + Droppables.reset(); + }, + keyPress: function(event) { + if(this.active) { + if(event.keyCode==Event.KEY_ESC) { + this.finishDrag(event, false); + Event.stop(event); + } + } + }, + endDrag: function(event) { + if(this.active && this.dragging) { + this.finishDrag(event, true); + Event.stop(event); + } + this.active = false; + this.dragging = false; + }, + draw: function(event) { + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var offsets = Position.cumulativeOffset(this.element); + offsets[0] -= this.currentLeft(); + offsets[1] -= this.currentTop(); + var style = this.element.style; + if((!this.options.constraint) || (this.options.constraint=='horizontal')) + style.left = (pointer[0] - offsets[0] - this.offsetX) + "px"; + if((!this.options.constraint) || (this.options.constraint=='vertical')) + style.top = (pointer[1] - offsets[1] - this.offsetY) + "px"; + if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering + }, + update: function(event) { + if(this.active) { + if(!this.dragging) { + var style = this.element.style; + this.dragging = true; + + if(Element.getStyle(this.element,'position')=='') + style.position = "relative"; + + if(this.options.zindex) { + this.options.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); + style.zIndex = this.options.zindex; + } + + if(this.options.ghosting) { + this._clone = this.element.cloneNode(true); + Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + + Draggables.notify('onStart', this); + if(this.options.starteffect) this.options.starteffect(this.element); + } + + Droppables.show(event, this.element); + this.draw(event); + if(this.options.change) this.options.change(this); + + // fix AppleWebKit rendering + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + + Event.stop(event); + } + } +} + +/*--------------------------------------------------------------------------*/ + +var SortableObserver = Class.create(); +SortableObserver.prototype = { + initialize: function(element, observer) { + this.element = $(element); + this.observer = observer; + this.lastValue = Sortable.serialize(this.element); + }, + onStart: function() { + this.lastValue = Sortable.serialize(this.element); + }, + onEnd: function() { + Sortable.unmark(); + if(this.lastValue != Sortable.serialize(this.element)) + this.observer(this.element) + } +} + +var Sortable = { + sortables: new Array(), + options: function(element){ + element = $(element); + return this.sortables.detect(function(s) { return s.element == element }); + }, + destroy: function(element){ + element = $(element); + this.sortables.findAll(function(s) { return s.element == element }).each(function(s){ + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + }); + this.sortables = this.sortables.reject(function(s) { return s.element == element }); + }, + create: function(element) { + element = $(element); + var options = Object.extend({ + element: element, + tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + tree: false, // fixme: unimplemented + overlap: 'vertical', // one of 'vertical', 'horizontal' + constraint: 'vertical', // one of 'vertical', 'horizontal', false + containment: element, // also takes array of elements (or id's); or false + handle: false, // or a CSS class + only: false, + hoverclass: null, + ghosting: false, + format: null, + onChange: function() {}, + onUpdate: function() {} + }, arguments[1] || {}); + + // clear any old sortable with same element + this.destroy(element); + + // build options for the draggables + var options_for_draggable = { + revert: true, + ghosting: options.ghosting, + constraint: options.constraint, + handle: options.handle }; + + if(options.starteffect) + options_for_draggable.starteffect = options.starteffect; + + if(options.reverteffect) + options_for_draggable.reverteffect = options.reverteffect; + else + if(options.ghosting) options_for_draggable.reverteffect = function(element) { + element.style.top = 0; + element.style.left = 0; + }; + + if(options.endeffect) + options_for_draggable.endeffect = options.endeffect; + + if(options.zindex) + options_for_draggable.zindex = options.zindex; + + // build options for the droppables + var options_for_droppable = { + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass, + onHover: Sortable.onHover, + greedy: !options.dropOnEmpty + } + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // make it so + + // drop on empty handling + if(options.dropOnEmpty) { + Droppables.add(element, + {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false}); + options.droppables.push(element); + } + + (this.findElements(element, options) || []).each( function(e) { + // handles are per-draggable + var handle = options.handle ? + Element.Class.childrenWith(e, options.handle)[0] : e; + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + options.droppables.push(e); + }); + + // keep reference + this.sortables.push(options); + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + if(!element.hasChildNodes()) return null; + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName==options.tag.toUpperCase() && + (!options.only || (Element.Class.has(e, options.only)))) + elements.push(e); + if(options.tree) { + var grandchildren = this.findElements(e, options); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : null); + }, + + onHover: function(element, dropon, overlap) { + if(overlap>0.5) { + Sortable.mark(dropon, 'before'); + if(dropon.previousSibling != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } else { + Sortable.mark(dropon, 'after'); + var nextElement = dropon.nextSibling || null; + if(nextElement != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, nextElement); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } + }, + + onEmptyHover: function(element, dropon) { + if(element.parentNode!=dropon) { + dropon.appendChild(element); + } + }, + + unmark: function() { + if(Sortable._marker) Element.hide(Sortable._marker); + }, + + mark: function(dropon, position) { + // mark on ghosting only + var sortable = Sortable.options(dropon.parentNode); + if(sortable && !sortable.ghosting) return; + + if(!Sortable._marker) { + Sortable._marker = $('dropmarker') || document.createElement('DIV'); + Element.hide(Sortable._marker); + Element.Class.add(Sortable._marker, 'dropmarker'); + Sortable._marker.style.position = 'absolute'; + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = Position.cumulativeOffset(dropon); + Sortable._marker.style.top = offsets[1] + 'px'; + if(position=='after') Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px'; + Sortable._marker.style.left = offsets[0] + 'px'; + Element.show(Sortable._marker); + }, + + serialize: function(element) { + element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format || /^[^_]*_(.*)$/ + }, arguments[1] || {}); + return $(this.findElements(element, options) || []).collect( function(item) { + return (encodeURIComponent(options.name) + "[]=" + + encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : '')); + }).join("&"); + } +} \ No newline at end of file diff --git a/public/javascripts/edit_web.js b/public/javascripts/edit_web.js new file mode 100644 index 00000000..38b1ba49 --- /dev/null +++ b/public/javascripts/edit_web.js @@ -0,0 +1,55 @@ +function proposeAddress() { + document.getElementById('address').value = + document.getElementById('name').value.replace(/[^a-zA-Z0-9]/g, "").toLowerCase(); +} + +function cleanAddress() { + document.getElementById('address').value = + document.getElementById('address').value.replace(/[^a-zA-Z0-9]/g, "").toLowerCase(); +} + +function checkSystemPassword(password) { + if (password == "") { + alert("You must enter the system password"); + return false; + } else { + return true; + } +} + +function validateEditWebForm() { + if (!checkSystemPassword(document.getElementById('system_password').value)) { + return false; + } + if (document.getElementById('name').value == "") { + alert("You must pick a name for the web"); + return false; + } + if (document.getElementById('address').value == "") { + alert("You must pick an address for the web"); + return false; + } + if (document.getElementById('password').value != "" && + document.getElementById('password').value != document.getElementById('password_check').value) { + alert("The password and its verification doesn't match"); + return false; + } + return true; +} + +// overriding auto-complete by form managers +// code by Chris Holland, lifted from +// http://chrisholland.blogspot.com/2004/11/banks-protect-privacy-disable.html +function overrideAutocomplete() { + if (document.getElementsByTagName) { + var inputElements = document.getElementsByTagName("input"); + for (i=0; inputElements[i]; i++) { + if (inputElements[i].className && (inputElements[i].className.indexOf("disableAutoComplete") != -1)) { + inputElements[i].setAttribute("autocomplete","off"); + }//if current input element has the disableAutoComplete class set. + }//loop thru input elements + } +} + +// This line is executed when the script is loaded +overrideAutocomplete(); diff --git a/public/javascripts/effects.js b/public/javascripts/effects.js new file mode 100644 index 00000000..7e65d922 --- /dev/null +++ b/public/javascripts/effects.js @@ -0,0 +1,1101 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// See scriptaculous.js for full license. + +Object.debug = function(obj) { + var info = []; + + if(typeof obj in ["string","number"]) { + return obj; + } else { + for(property in obj) + if(typeof obj[property]!="function") + info.push(property + ' => ' + + (typeof obj[property] == "string" ? + '"' + obj[property] + '"' : + obj[property])); + } + + return ("'" + obj + "' #" + typeof obj + + ": {" + info.join(", ") + "}"); +} + + +/*--------------------------------------------------------------------------*/ + +var Builder = { + NODEMAP: { + AREA: 'map', + CAPTION: 'table', + COL: 'table', + COLGROUP: 'table', + LEGEND: 'fieldset', + OPTGROUP: 'select', + OPTION: 'select', + PARAM: 'object', + TBODY: 'table', + TD: 'table', + TFOOT: 'table', + TH: 'table', + THEAD: 'table', + TR: 'table' + }, + // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken, + // due to a Firefox bug + node: function(elementName) { + elementName = elementName.toUpperCase(); + + // try innerHTML approach + var parentTag = this.NODEMAP[elementName] || 'div'; + var parentElement = document.createElement(parentTag); + parentElement.innerHTML = "<" + elementName + ">"; + var element = parentElement.firstChild || null; + + // see if browser added wrapping tags + if(element && (element.tagName != elementName)) + element = element.getElementsByTagName(elementName)[0]; + + // fallback to createElement approach + if(!element) element = document.createElement(elementName); + + // abort if nothing could be created + if(!element) return; + + // attributes (or text) + if(arguments[1]) + if(this._isStringOrNumber(arguments[1]) || + (arguments[1] instanceof Array)) { + this._children(element, arguments[1]); + } else { + var attrs = this._attributes(arguments[1]); + if(attrs.length) { + parentElement.innerHTML = "<" +elementName + " " + + attrs + ">"; + element = parentElement.firstChild || null; + // workaround firefox 1.0.X bug + if(!element) { + element = document.createElement(elementName); + for(attr in arguments[1]) + element[attr == 'class' ? 'className' : attr] = arguments[1][attr]; + } + if(element.tagName != elementName) + element = parentElement.getElementsByTagName(elementName)[0]; + } + } + + // text, or array of children + if(arguments[2]) + this._children(element, arguments[2]); + + return element; + }, + _text: function(text) { + return document.createTextNode(text); + }, + _attributes: function(attributes) { + var attrs = []; + for(attribute in attributes) + attrs.push((attribute=='className' ? 'class' : attribute) + + '="' + attributes[attribute].toString().escapeHTML() + '"'); + return attrs.join(" "); + }, + _children: function(element, children) { + if(typeof children=='object') { // array can hold nodes and text + children.flatten().each( function(e) { + if(typeof e=='object') + element.appendChild(e) + else + if(Builder._isStringOrNumber(e)) + element.appendChild(Builder._text(e)); + }); + } else + if(Builder._isStringOrNumber(children)) + element.appendChild(Builder._text(children)); + }, + _isStringOrNumber: function(param) { + return(typeof param=='string' || typeof param=='number'); + } +} + +/* ------------- element ext -------------- */ + +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + color = "#"; + if(this.slice(0,4) == "rgb(") { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if(this.slice(0,1) == '#') { + if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if(this.length==7) color = this.toLowerCase(); + } + } + return(color.length==7 ? color : (arguments[0] || this)); +} + +Element.collectTextNodesIgnoreClass = function(element, ignoreclass) { + var children = $(element).childNodes; + var text = ""; + var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i"); + + for (var i = 0; i < children.length; i++) { + if(children[i].nodeType==3) { + text+=children[i].nodeValue; + } else { + if((!children[i].className.match(classtest)) && children[i].hasChildNodes()) + text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass); + } + } + + return text; +} + +Element.setContentZoom = function(element, percent) { + element = $(element); + element.style.fontSize = (percent/100) + "em"; + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); +} + +Element.getOpacity = function(element){ + var opacity; + if (opacity = Element.getStyle(element, "opacity")) + return parseFloat(opacity); + if (opacity = (Element.getStyle(element, "filter") || '').match(/alpha\(opacity=(.*)\)/)) + if(opacity[1]) return parseFloat(opacity[1]) / 100; + return 1.0; +} + +Element.setOpacity = function(element, value){ + element= $(element); + var els = element.style; + if (value == 1){ + els.opacity = '0.999999'; + if(/MSIE/.test(navigator.userAgent)) + els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,''); + } else { + if(value < 0.00001) value = 0; + els.opacity = value; + if(/MSIE/.test(navigator.userAgent)) + els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') + + "alpha(opacity="+value*100+")"; + } +} + +Element.getInlineOpacity = function(element){ + element= $(element); + var op; + op = element.style.opacity; + if (typeof op != "undefined" && op != "") return op; + return ""; +} + +Element.setInlineOpacity = function(element, value){ + element= $(element); + var els = element.style; + els.opacity = value; +} + +/*--------------------------------------------------------------------------*/ + +Element.Class = { + // Element.toggleClass(element, className) toggles the class being on/off + // Element.toggleClass(element, className1, className2) toggles between both classes, + // defaulting to className1 if neither exist + toggle: function(element, className) { + if(Element.Class.has(element, className)) { + Element.Class.remove(element, className); + if(arguments.length == 3) Element.Class.add(element, arguments[2]); + } else { + Element.Class.add(element, className); + if(arguments.length == 3) Element.Class.remove(element, arguments[2]); + } + }, + + // gets space-delimited classnames of an element as an array + get: function(element) { + return $(element).className.split(' '); + }, + + // functions adapted from original functions by Gavin Kistner + remove: function(element) { + element = $(element); + var removeClasses = arguments; + $R(1,arguments.length-1).each( function(index) { + element.className = + element.className.split(' ').reject( + function(klass) { return (klass == removeClasses[index]) } ).join(' '); + }); + }, + + add: function(element) { + element = $(element); + for(var i = 1; i < arguments.length; i++) { + Element.Class.remove(element, arguments[i]); + element.className += (element.className.length > 0 ? ' ' : '') + arguments[i]; + } + }, + + // returns true if all given classes exist in said element + has: function(element) { + element = $(element); + if(!element || !element.className) return false; + var regEx; + for(var i = 1; i < arguments.length; i++) { + if((typeof arguments[i] == 'object') && + (arguments[i].constructor == Array)) { + for(var j = 0; j < arguments[i].length; j++) { + regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)"); + if(!regEx.test(element.className)) return false; + } + } else { + regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)"); + if(!regEx.test(element.className)) return false; + } + } + return true; + }, + + // expects arrays of strings and/or strings as optional paramters + // Element.Class.has_any(element, ['classA','classB','classC'], 'classD') + has_any: function(element) { + element = $(element); + if(!element || !element.className) return false; + var regEx; + for(var i = 1; i < arguments.length; i++) { + if((typeof arguments[i] == 'object') && + (arguments[i].constructor == Array)) { + for(var j = 0; j < arguments[i].length; j++) { + regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)"); + if(regEx.test(element.className)) return true; + } + } else { + regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)"); + if(regEx.test(element.className)) return true; + } + } + return false; + }, + + childrenWith: function(element, className) { + var children = $(element).getElementsByTagName('*'); + var elements = new Array(); + + for (var i = 0; i < children.length; i++) + if (Element.Class.has(children[i], className)) + elements.push(children[i]); + + return elements; + } +} + +/*--------------------------------------------------------------------------*/ + +var Effect = { + tagifyText: function(element) { + var tagifyStyle = "position:relative"; + if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ";zoom:1"; + element = $(element); + $A(element.childNodes).each( function(child) { + if(child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + Builder.node('span',{style: tagifyStyle}, + character == " " ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if(((typeof element == 'object') || + (typeof element == 'function')) && + (element.length)) + elements = element; + else + elements = $(element).childNodes; + + var options = Object.extend({ + speed: 0.1, + delay: 0.0 + }, arguments[2] || {}); + var speed = options.speed; + var delay = options.delay; + + $A(elements).each( function(element, index) { + new effect(element, Object.extend(options, { delay: delay + index * speed })); + }); + } +}; + +var Effect2 = Effect; // deprecated + +/* ------------- transitions ------------- */ + +Effect.Transitions = {} + +Effect.Transitions.linear = function(pos) { + return pos; +} +Effect.Transitions.sinoidal = function(pos) { + return (-Math.cos(pos*Math.PI)/2) + 0.5; +} +Effect.Transitions.reverse = function(pos) { + return 1-pos; +} +Effect.Transitions.flicker = function(pos) { + return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; +} +Effect.Transitions.wobble = function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; +} +Effect.Transitions.pulse = function(pos) { + return (Math.floor(pos*10) % 2 == 0 ? + (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10))); +} +Effect.Transitions.none = function(pos) { + return 0; +} +Effect.Transitions.full = function(pos) { + return 1; +} + +/* ------------- core effects ------------- */ + +Effect.Queue = { + effects: [], + interval: null, + add: function(effect) { + var timestamp = new Date().getTime(); + + switch(effect.options.queue) { + case 'front': + // move unstarted effects after this effect + this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + }); + break; + case 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + this.effects.push(effect); + if(!this.interval) + this.interval = setInterval(this.loop.bind(this), 40); + }, + remove: function(effect) { + this.effects = this.effects.reject(function(e) { return e==effect }); + if(this.effects.length == 0) { + clearInterval(this.interval); + this.interval = null; + } + }, + loop: function() { + var timePos = new Date().getTime(); + this.effects.invoke('loop', timePos); + } +} + +Effect.Base = function() {}; +Effect.Base.prototype = { + position: null, + setOptions: function(options) { + this.options = Object.extend({ + transition: Effect.Transitions.sinoidal, + duration: 1.0, // seconds + fps: 25.0, // max. 25fps due to Effect.Queue implementation + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' + }, options || {}); + }, + start: function(options) { + this.setOptions(options || {}); + this.currentFrame = 0; + this.state = 'idle'; + this.startOn = this.options.delay*1000; + this.finishOn = this.startOn + (this.options.duration*1000); + this.event('beforeStart'); + if(!this.options.sync) Effect.Queue.add(this); + }, + loop: function(timePos) { + if(timePos >= this.startOn) { + if(timePos >= this.finishOn) { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + if(this.finish) this.finish(); + this.event('afterFinish'); + return; + } + var pos = (timePos - this.startOn) / (this.finishOn - this.startOn); + var frame = Math.round(pos * this.options.fps * this.options.duration); + if(frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + render: function(pos) { + if(this.state == 'idle') { + this.state = 'running'; + this.event('beforeSetup'); + if(this.setup) this.setup(); + this.event('afterSetup'); + } + if(this.options.transition) pos = this.options.transition(pos); + pos *= (this.options.to-this.options.from); + pos += this.options.from; + this.position = pos; + this.event('beforeUpdate'); + if(this.update) this.update(pos); + this.event('afterUpdate'); + }, + cancel: function() { + if(!this.options.sync) Effect.Queue.remove(this); + this.state = 'finished'; + }, + event: function(eventName) { + if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if(this.options[eventName]) this.options[eventName](this); + } +} + +Effect.Parallel = Class.create(); +Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { + initialize: function(effects) { + this.effects = effects || []; + this.start(arguments[1]); + }, + update: function(position) { + this.effects.invoke('render', position); + }, + finish: function(position) { + this.effects.each( function(effect) { + effect.render(1.0); + effect.cancel(); + effect.event('beforeFinish'); + if(effect.finish) effect.finish(position); + effect.event('afterFinish'); + }); + } +}); + +Effect.Opacity = Class.create(); +Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + // make this work on IE on elements without 'layout' + if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout)) + this.element.style.zoom = 1; + var options = Object.extend({ + from: Element.getOpacity(this.element) || 0.0, + to: 1.0 + }, arguments[1] || {}); + this.start(options); + }, + update: function(position) { + Element.setOpacity(this.element, position); + } +}); + +Effect.MoveBy = Class.create(); +Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), { + initialize: function(element, toTop, toLeft) { + this.element = $(element); + this.toTop = toTop; + this.toLeft = toLeft; + this.start(arguments[3]); + }, + setup: function() { + // Bug in Opera: Opera returns the "real" position of a static element or + // relative element that does not have top/left explicitly set. + // ==> Always set top and left for position relative elements in your stylesheets + // (to 0 if you do not need them) + + Element.makePositioned(this.element); + this.originalTop = parseFloat(Element.getStyle(this.element,'top') || '0'); + this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0'); + }, + update: function(position) { + var topd = this.toTop * position + this.originalTop; + var leftd = this.toLeft * position + this.originalLeft; + this.setPosition(topd, leftd); + }, + setPosition: function(topd, leftd) { + this.element.style.top = topd + "px"; + this.element.style.left = leftd + "px"; + } +}); + +Effect.Scale = Class.create(); +Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { + initialize: function(element, percent) { + this.element = $(element) + var options = Object.extend({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or {} with provided values + scaleFrom: 100.0, + scaleTo: percent + }, arguments[2] || {}); + this.start(options); + }, + setup: function() { + var effect = this; + + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = Element.getStyle(this.element,'position'); + + effect.originalStyle = {}; + ['top','left','width','height','fontSize'].each( function(k) { + effect.originalStyle[k] = effect.element.style[k]; + }); + + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + + var fontSize = Element.getStyle(this.element,'font-size') || "100%"; + ['em','px','%'].each( function(fontSizeType) { + if(fontSize.indexOf(fontSizeType)>0) { + effect.fontSize = parseFloat(fontSize); + effect.fontSizeType = fontSizeType; + } + }); + + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + + this.dims = null; + if(this.options.scaleMode=='box') + this.dims = [this.element.clientHeight, this.element.clientWidth]; + if(this.options.scaleMode=='content') + this.dims = [this.element.scrollHeight, this.element.scrollWidth]; + if(!this.dims) + this.dims = [this.options.scaleMode.originalHeight, + this.options.scaleMode.originalWidth]; + }, + update: function(position) { + var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if(this.options.scaleContent && this.fontSize) + this.element.style.fontSize = this.fontSize*currentScale + this.fontSizeType; + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if (this.restoreAfterFinish) { + var effect = this; + ['top','left','width','height','fontSize'].each( function(k) { + effect.element.style[k] = effect.originalStyle[k]; + }); + } + }, + setDimensions: function(height, width) { + var els = this.element.style; + if(this.options.scaleX) els.width = width + 'px'; + if(this.options.scaleY) els.height = height + 'px'; + if(this.options.scaleFromCenter) { + var topd = (height - this.dims[0])/2; + var leftd = (width - this.dims[1])/2; + if(this.elementPositioning == 'absolute') { + if(this.options.scaleY) els.top = this.originalTop-topd + "px"; + if(this.options.scaleX) els.left = this.originalLeft-leftd + "px"; + } else { + if(this.options.scaleY) els.top = -topd + "px"; + if(this.options.scaleX) els.left = -leftd + "px"; + } + } + } +}); + +Effect.Highlight = Class.create(); +Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + var options = Object.extend({ + startcolor: "#ffff99" + }, arguments[1] || {}); + this.start(options); + }, + setup: function() { + // Disable background image during the effect + this.oldBgImage = this.element.style.backgroundImage; + this.element.style.backgroundImage = "none"; + if(!this.options.endcolor) + this.options.endcolor = Element.getStyle(this.element, 'background-color').parseColor('#ffffff'); + if (typeof this.options.restorecolor == "undefined") + this.options.restorecolor = this.element.style.backgroundColor; + // init color calculations + this.colors_base = [ + parseInt(this.options.startcolor.slice(1,3),16), + parseInt(this.options.startcolor.slice(3,5),16), + parseInt(this.options.startcolor.slice(5),16) ]; + this.colors_delta = [ + parseInt(this.options.endcolor.slice(1,3),16)-this.colors_base[0], + parseInt(this.options.endcolor.slice(3,5),16)-this.colors_base[1], + parseInt(this.options.endcolor.slice(5),16)-this.colors_base[2]]; + }, + update: function(position) { + var effect = this; var colors = $R(0,2).map( function(i){ + return Math.round(effect.colors_base[i]+(effect.colors_delta[i]*position)) + }); + this.element.style.backgroundColor = "#" + + colors[0].toColorPart() + colors[1].toColorPart() + colors[2].toColorPart(); + }, + finish: function() { + this.element.style.backgroundColor = this.options.restorecolor; + this.element.style.backgroundImage = this.oldBgImage; + } +}); + +Effect.ScrollTo = Class.create(); +Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + this.start(arguments[1] || {}); + }, + setup: function() { + Position.prepare(); + var offsets = Position.cumulativeOffset(this.element); + var max = window.innerHeight ? + window.height - window.innerHeight : + document.body.scrollHeight - + (document.documentElement.clientHeight ? + document.documentElement.clientHeight : document.body.clientHeight); + this.scrollStart = Position.deltaY; + this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart; + }, + update: function(position) { + Position.prepare(); + window.scrollTo(Position.deltaX, + this.scrollStart + (position*this.delta)); + } +}); + +/* ------------- combination effects ------------- */ + +Effect.Fade = function(element) { + var oldOpacity = Element.getInlineOpacity(element); + var options = Object.extend({ + from: Element.getOpacity(element) || 1.0, + to: 0.0, + afterFinishInternal: function(effect) + { if (effect.options.to == 0) { + Element.hide(effect.element); + Element.setInlineOpacity(effect.element, oldOpacity); + } + } + }, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Appear = function(element) { + var options = Object.extend({ + from: (Element.getStyle(element, "display") == "none" ? 0.0 : Element.getOpacity(element) || 0.0), + to: 1.0, + beforeSetup: function(effect) + { Element.setOpacity(effect.element, effect.options.from); + Element.show(effect.element); } + }, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Puff = function(element) { + element = $(element); + var oldOpacity = Element.getInlineOpacity(element); + var oldPosition = element.style.position; + return new Effect.Parallel( + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) + { effect.effects[0].element.style.position = 'absolute'; }, + afterFinishInternal: function(effect) + { Element.hide(effect.effects[0].element); + effect.effects[0].element.style.position = oldPosition; + Element.setInlineOpacity(effect.effects[0].element, oldOpacity); } + }, arguments[1] || {}) + ); +} + +Effect.BlindUp = function(element) { + element = $(element); + Element.makeClipping(element); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) + { + Element.hide(effect.element); + Element.undoClipping(effect.element); + } + }, arguments[1] || {}) + ); +} + +Effect.BlindDown = function(element) { + element = $(element); + var oldHeight = element.style.height; + var elementDimensions = Element.getDimensions(element); + return new Effect.Scale(element, 100, + Object.extend({ scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + Element.makeClipping(effect.element); + effect.element.style.height = "0px"; + Element.show(effect.element); + }, + afterFinishInternal: function(effect) { + Element.undoClipping(effect.element); + effect.element.style.height = oldHeight; + } + }, arguments[1] || {}) + ); +} + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = Element.getInlineOpacity(element); + return new Effect.Appear(element, { + duration: 0.4, + from: 0, + transition: Effect.Transitions.flicker, + afterFinishInternal: function(effect) { + new Effect.Scale(effect.element, 1, { + duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, restoreAfterFinish: true, + beforeSetup: function(effect) { + Element.makePositioned(effect.element); + Element.makeClipping(effect.element); + }, + afterFinishInternal: function(effect) { + Element.hide(effect.element); + Element.undoClipping(effect.element); + Element.undoPositioned(effect.element); + Element.setInlineOpacity(effect.element, oldOpacity); + } + }) + } + }); +} + +Effect.DropOut = function(element) { + element = $(element); + var oldTop = element.style.top; + var oldLeft = element.style.left; + var oldOpacity = Element.getInlineOpacity(element); + return new Effect.Parallel( + [ new Effect.MoveBy(element, 100, 0, { sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + Element.makePositioned(effect.effects[0].element); }, + afterFinishInternal: function(effect) { + Element.hide(effect.effects[0].element); + Element.undoPositioned(effect.effects[0].element); + effect.effects[0].element.style.left = oldLeft; + effect.effects[0].element.style.top = oldTop; + Element.setInlineOpacity(effect.effects[0].element, oldOpacity); } + }, arguments[1] || {})); +} + +Effect.Shake = function(element) { + element = $(element); + var oldTop = element.style.top; + var oldLeft = element.style.left; + return new Effect.MoveBy(element, 0, 20, + { duration: 0.05, afterFinishInternal: function(effect) { + new Effect.MoveBy(effect.element, 0, -40, + { duration: 0.1, afterFinishInternal: function(effect) { + new Effect.MoveBy(effect.element, 0, 40, + { duration: 0.1, afterFinishInternal: function(effect) { + new Effect.MoveBy(effect.element, 0, -40, + { duration: 0.1, afterFinishInternal: function(effect) { + new Effect.MoveBy(effect.element, 0, 40, + { duration: 0.1, afterFinishInternal: function(effect) { + new Effect.MoveBy(effect.element, 0, -20, + { duration: 0.05, afterFinishInternal: function(effect) { + Element.undoPositioned(effect.element); + effect.element.style.left = oldLeft; + effect.element.style.top = oldTop; + }}) }}) }}) }}) }}) }}); +} + +Effect.SlideDown = function(element) { + element = $(element); + Element.cleanWhitespace(element); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = element.firstChild.style.bottom; + var elementDimensions = Element.getDimensions(element); + return new Effect.Scale(element, 100, + Object.extend({ scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + Element.makePositioned(effect.element.firstChild); + if (window.opera) effect.element.firstChild.style.top = ""; + Element.makeClipping(effect.element); + element.style.height = '0'; + Element.show(element); + }, + afterUpdateInternal: function(effect) { + effect.element.firstChild.style.bottom = + (effect.originalHeight - effect.element.clientHeight) + 'px'; }, + afterFinishInternal: function(effect) { + Element.undoClipping(effect.element); + Element.undoPositioned(effect.element.firstChild); + effect.element.firstChild.style.bottom = oldInnerBottom; } + }, arguments[1] || {}) + ); +} + +Effect.SlideUp = function(element) { + element = $(element); + Element.cleanWhitespace(element); + var oldInnerBottom = element.firstChild.style.bottom; + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + restoreAfterFinish: true, + beforeStartInternal: function(effect) { + Element.makePositioned(effect.element.firstChild); + if (window.opera) effect.element.firstChild.style.top = ""; + Element.makeClipping(effect.element); + Element.show(element); + }, + afterUpdateInternal: function(effect) { + effect.element.firstChild.style.bottom = + (effect.originalHeight - effect.element.clientHeight) + 'px'; }, + afterFinishInternal: function(effect) { + Element.hide(effect.element); + Element.undoClipping(effect.element); + Element.undoPositioned(effect.element.firstChild); + effect.element.firstChild.style.bottom = oldInnerBottom; } + }, arguments[1] || {}) + ); +} + +Effect.Squish = function(element) { + // Bug in opera makes the TD containing this element expand for a instance after finish + return new Effect.Scale(element, window.opera ? 1 : 0, + { restoreAfterFinish: true, + beforeSetup: function(effect) { + Element.makeClipping(effect.element); }, + afterFinishInternal: function(effect) { + Element.hide(effect.element); + Element.undoClipping(effect.element); } + }); +} + +Effect.Grow = function(element) { + element = $(element); + var options = arguments[1] || {}; + + var elementDimensions = Element.getDimensions(element); + var originalWidth = elementDimensions.width; + var originalHeight = elementDimensions.height; + var oldTop = element.style.top; + var oldLeft = element.style.left; + var oldHeight = element.style.height; + var oldWidth = element.style.width; + var oldOpacity = Element.getInlineOpacity(element); + + var direction = options.direction || 'center'; + var moveTransition = options.moveTransition || Effect.Transitions.sinoidal; + var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal; + var opacityTransition = options.opacityTransition || Effect.Transitions.full; + + var initialMoveX, initialMoveY; + var moveX, moveY; + + switch (direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = originalWidth; + initialMoveY = moveY = 0; + moveX = -originalWidth; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = originalHeight; + moveY = -originalHeight; + break; + case 'bottom-right': + initialMoveX = originalWidth; + initialMoveY = originalHeight; + moveX = -originalWidth; + moveY = -originalHeight; + break; + case 'center': + initialMoveX = originalWidth / 2; + initialMoveY = originalHeight / 2; + moveX = -originalWidth / 2; + moveY = -originalHeight / 2; + break; + } + + return new Effect.MoveBy(element, initialMoveY, initialMoveX, { + duration: 0.01, + beforeSetup: function(effect) { + Element.hide(effect.element); + Element.makeClipping(effect.element); + Element.makePositioned(effect.element); + }, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: opacityTransition }), + new Effect.MoveBy(effect.element, moveY, moveX, { sync: true, transition: moveTransition }), + new Effect.Scale(effect.element, 100, { + scaleMode: { originalHeight: originalHeight, originalWidth: originalWidth }, + sync: true, scaleFrom: window.opera ? 1 : 0, transition: scaleTransition, restoreAfterFinish: true}) + ], Object.extend({ + beforeSetup: function(effect) { + effect.effects[0].element.style.height = 0; + Element.show(effect.effects[0].element); + }, + afterFinishInternal: function(effect) { + var el = effect.effects[0].element; + var els = el.style; + Element.undoClipping(el); + Element.undoPositioned(el); + els.top = oldTop; + els.left = oldLeft; + els.height = oldHeight; + els.width = originalWidth; + Element.setInlineOpacity(el, oldOpacity); + } + }, options) + ) + } + }); +} + +Effect.Shrink = function(element) { + element = $(element); + var options = arguments[1] || {}; + + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + var oldTop = element.style.top; + var oldLeft = element.style.left; + var oldHeight = element.style.height; + var oldWidth = element.style.width; + var oldOpacity = Element.getInlineOpacity(element); + + var direction = options.direction || 'center'; + var moveTransition = options.moveTransition || Effect.Transitions.sinoidal; + var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal; + var opacityTransition = options.opacityTransition || Effect.Transitions.none; + + var moveX, moveY; + + switch (direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = originalWidth; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = originalHeight; + break; + case 'bottom-right': + moveX = originalWidth; + moveY = originalHeight; + break; + case 'center': + moveX = originalWidth / 2; + moveY = originalHeight / 2; + break; + } + + return new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: opacityTransition }), + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: scaleTransition, restoreAfterFinish: true}), + new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + Element.makePositioned(effect.effects[0].element); + Element.makeClipping(effect.effects[0].element); + }, + afterFinishInternal: function(effect) { + var el = effect.effects[0].element; + var els = el.style; + Element.hide(el); + Element.undoClipping(el); + Element.undoPositioned(el); + els.top = oldTop; + els.left = oldLeft; + els.height = oldHeight; + els.width = oldWidth; + Element.setInlineOpacity(el, oldOpacity); + } + }, options) + ); +} + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || {}; + var oldOpacity = Element.getInlineOpacity(element); + var transition = options.transition || Effect.Transitions.sinoidal; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) }; + reverser.bind(transition); + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 3.0, from: 0, + afterFinishInternal: function(effect) { Element.setInlineOpacity(effect.element, oldOpacity); } + }, options), {transition: reverser})); +} + +Effect.Fold = function(element) { + element = $(element); + var originalTop = element.style.top; + var originalLeft = element.style.left; + var originalWidth = element.style.width; + var originalHeight = element.style.height; + Element.makeClipping(element); + return new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function(effect) { + Element.hide(effect.element); + Element.undoClipping(effect.element); + effect.element.style.top = originalTop; + effect.element.style.left = originalLeft; + effect.element.style.width = originalWidth; + effect.element.style.height = originalHeight; + } }); + }}, arguments[1] || {})); +} diff --git a/public/javascripts/prototype.js b/public/javascripts/prototype.js new file mode 100644 index 00000000..120f4cb9 --- /dev/null +++ b/public/javascripts/prototype.js @@ -0,0 +1,1724 @@ +/* Prototype JavaScript framework, version 1.4.0_rc0 + * (c) 2005 Sam Stephenson + * + * THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff + * against the source tree, available from the Prototype darcs repository. + * + * Prototype is freely distributable under the terms of an MIT-style license. + * + * For details, see the Prototype web site: http://prototype.conio.net/ + * +/*--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.4.0_rc0', + + emptyFunction: function() {}, + K: function(x) {return x} +} + +var Class = { + create: function() { + return function() { + this.initialize.apply(this, arguments); + } + } +} + +var Abstract = new Object(); + +Object.extend = function(destination, source) { + for (property in source) { + destination[property] = source[property]; + } + return destination; +} + +Object.inspect = function(object) { + try { + if (object == undefined) return 'undefined'; + if (object == null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } +} + +Function.prototype.bind = function(object) { + var __method = this; + return function() { + return __method.apply(object, arguments); + } +} + +Function.prototype.bindAsEventListener = function(object) { + var __method = this; + return function(event) { + return __method.call(object, event || window.event); + } +} + +Object.extend(Number.prototype, { + toColorPart: function() { + var digits = this.toString(16); + if (this < 16) return '0' + digits; + return digits; + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + } +}); + +var Try = { + these: function() { + var returnValue; + + for (var i = 0; i < arguments.length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) {} + } + + return returnValue; + } +} + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create(); +PeriodicalExecuter.prototype = { + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.callback(); + } finally { + this.currentlyExecuting = false; + } + } + } +} + +/*--------------------------------------------------------------------------*/ + +function $() { + var elements = new Array(); + + for (var i = 0; i < arguments.length; i++) { + var element = arguments[i]; + if (typeof element == 'string') + element = document.getElementById(element); + + if (arguments.length == 1) + return element; + + elements.push(element); + } + + return elements; +} +Object.extend(String.prototype, { + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + escapeHTML: function() { + var div = document.createElement('div'); + var text = document.createTextNode(this); + div.appendChild(text); + return div.innerHTML; + }, + + unescapeHTML: function() { + var div = document.createElement('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? div.childNodes[0].nodeValue : ''; + }, + + toQueryParams: function() { + var pairs = this.match(/^\??(.*)$/)[1].split('&'); + return pairs.inject({}, function(params, pairString) { + var pair = pairString.split('='); + params[pair[0]] = pair[1]; + return params; + }); + }, + + toArray: function() { + return this.split(''); + }, + + camelize: function() { + var oStringList = this.split('-'); + if (oStringList.length == 1) return oStringList[0]; + + var camelizedString = this.indexOf('-') == 0 + ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) + : oStringList[0]; + + for (var i = 1, len = oStringList.length; i < len; i++) { + var s = oStringList[i]; + camelizedString += s.charAt(0).toUpperCase() + s.substring(1); + } + + return camelizedString; + }, + + inspect: function() { + return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'"; + } +}); + +String.prototype.parseQuery = String.prototype.toQueryParams; + +var $break = new Object(); +var $continue = new Object(); + +var Enumerable = { + each: function(iterator) { + var index = 0; + try { + this._each(function(value) { + try { + iterator(value, index++); + } catch (e) { + if (e != $continue) throw e; + } + }); + } catch (e) { + if (e != $break) throw e; + } + }, + + all: function(iterator) { + var result = true; + this.each(function(value, index) { + if (!(result &= (iterator || Prototype.K)(value, index))) + throw $break; + }); + return result; + }, + + any: function(iterator) { + var result = true; + this.each(function(value, index) { + if (result &= (iterator || Prototype.K)(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator) { + var results = []; + this.each(function(value, index) { + results.push(iterator(value, index)); + }); + return results; + }, + + detect: function (iterator) { + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator) { + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(pattern, iterator) { + var results = []; + this.each(function(value, index) { + var stringValue = value.toString(); + if (stringValue.match(pattern)) + results.push((iterator || Prototype.K)(value, index)); + }) + return results; + }, + + include: function(object) { + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inject: function(memo, iterator) { + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.collect(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (value >= (result || value)) + result = value; + }); + return result; + }, + + min: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (value <= (result || value)) + result = value; + }); + return result; + }, + + partition: function(iterator) { + var trues = [], falses = []; + this.each(function(value, index) { + ((iterator || Prototype.K)(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value, index) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator) { + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator) { + return this.collect(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.collect(Prototype.K); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (typeof args.last() == 'function') + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + iterator(value = collections.pluck(index)); + return value; + }); + }, + + inspect: function() { + return '#'; + } +} + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray +}); +var $A = Array.from = function(iterable) { + if (iterable.toArray) { + return iterable.toArray(); + } else { + var results = []; + for (var i = 0; i < iterable.length; i++) + results.push(iterable[i]); + return results; + } +} + +Object.extend(Array.prototype, Enumerable); + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0; i < this.length; i++) + iterator(this[i]); + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != undefined || value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(value.constructor == Array ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + indexOf: function(object) { + for (var i = 0; i < this.length; i++) + if (this[i] == object) return i; + return false; + }, + + reverse: function() { + var result = []; + for (var i = this.length; i > 0; i--) + result.push(this[i-1]); + return result; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } +}); +var Hash = { + _each: function(iterator) { + for (key in this) { + var value = this[key]; + if (typeof value == 'function') continue; + + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + merge: function(hash) { + return $H(hash).inject($H(this), function(mergedHash, pair) { + mergedHash[pair.key] = pair.value; + return mergedHash; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + return pair.map(encodeURIComponent).join('='); + }).join('&'); + }, + + inspect: function() { + return '#'; + } +} + +function $H(object) { + var hash = Object.extend({}, object || {}); + Object.extend(hash, Enumerable); + Object.extend(hash, Hash); + return hash; +} +var Range = Class.create(); +Object.extend(Range.prototype, Enumerable); +Object.extend(Range.prototype, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + do { + iterator(value); + value = value.succ(); + } while (this.include(value)); + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new Range(start, end, exclusive); +} + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')}, + function() {return new XMLHttpRequest()} + ) || false; + }, + + activeRequestCount: 0 +} + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responderToAdd) { + if (!this.include(responderToAdd)) + this.responders.push(responderToAdd); + }, + + unregister: function(responderToRemove) { + this.responders = this.responders.without(responderToRemove); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (responder[callback] && typeof responder[callback] == 'function') { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) { + } + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { + Ajax.activeRequestCount++; + }, + + onComplete: function() { + Ajax.activeRequestCount--; + } +}); + +Ajax.Base = function() {}; +Ajax.Base.prototype = { + setOptions: function(options) { + this.options = { + method: 'post', + asynchronous: true, + parameters: '' + } + Object.extend(this.options, options || {}); + }, + + responseIsSuccess: function() { + return this.transport.status == undefined + || this.transport.status == 0 + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + responseIsFailure: function() { + return !this.responseIsSuccess(); + } +} + +Ajax.Request = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + initialize: function(url, options) { + this.transport = Ajax.getTransport(); + this.setOptions(options); + this.request(url); + }, + + request: function(url) { + var parameters = this.options.parameters || ''; + if (parameters.length > 0) parameters += '&_='; + + try { + this.url = url; + if (this.options.method == 'get') + this.url += '?' + parameters; + + Ajax.Responders.dispatch('onCreate', this, this.transport); + + this.transport.open(this.options.method, this.url, + this.options.asynchronous); + + if (this.options.asynchronous) { + this.transport.onreadystatechange = this.onStateChange.bind(this); + setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); + } + + this.setRequestHeaders(); + + var body = this.options.postBody ? this.options.postBody : parameters; + this.transport.send(this.options.method == 'post' ? body : null); + + } catch (e) { + } + }, + + setRequestHeaders: function() { + var requestHeaders = + ['X-Requested-With', 'XMLHttpRequest', + 'X-Prototype-Version', Prototype.Version]; + + if (this.options.method == 'post') { + requestHeaders.push('Content-type', + 'application/x-www-form-urlencoded'); + + /* Force "Connection: close" for Mozilla browsers to work around + * a bug where XMLHttpReqeuest sends an incorrect Content-length + * header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType) + requestHeaders.push('Connection', 'close'); + } + + if (this.options.requestHeaders) + requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); + + for (var i = 0; i < requestHeaders.length; i += 2) + this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState != 1) + this.respondToReadyState(this.transport.readyState); + }, + + evalJSON: function() { + try { + var json = this.transport.getResponseHeader('X-JSON'), object; + object = eval(json); + return object; + } catch (e) { + } + }, + + respondToReadyState: function(readyState) { + var event = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (event == 'Complete') + (this.options['on' + this.transport.status] + || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(transport, json); + + (this.options['on' + event] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + event, this, transport, json); + + /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ + if (event == 'Complete') + this.transport.onreadystatechange = Prototype.emptyFunction; + } +}); + +Ajax.Updater = Class.create(); +Ajax.Updater.ScriptFragment = '(?:)((\n|.)*?)(?:<\/script>)'; + +Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { + initialize: function(container, url, options) { + this.containers = { + success: container.success ? $(container.success) : $(container), + failure: container.failure ? $(container.failure) : + (container.success ? null : $(container)) + } + + this.transport = Ajax.getTransport(); + this.setOptions(options); + + var onComplete = this.options.onComplete || Prototype.emptyFunction; + this.options.onComplete = (function(transport, object) { + this.updateContent(); + onComplete(transport, object); + }).bind(this); + + this.request(url); + }, + + updateContent: function() { + var receiver = this.responseIsSuccess() ? + this.containers.success : this.containers.failure; + + var match = new RegExp(Ajax.Updater.ScriptFragment, 'img'); + var response = this.transport.responseText.replace(match, ''); + var scripts = this.transport.responseText.match(match); + + if (receiver) { + if (this.options.insertion) { + new this.options.insertion(receiver, response); + } else { + receiver.innerHTML = response; + } + } + + if (this.responseIsSuccess()) { + if (this.onComplete) + setTimeout(this.onComplete.bind(this), 10); + } + + if (this.options.evalScripts && scripts) { + match = new RegExp(Ajax.Updater.ScriptFragment, 'im'); + setTimeout((function() { + for (var i = 0; i < scripts.length; i++) + eval(scripts[i].match(match)[1]); + }).bind(this), 10); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(); +Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { + initialize: function(container, url, options) { + this.setOptions(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = {}; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(request) { + if (this.options.decay) { + this.decay = (request.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = request.responseText; + } + this.timer = setTimeout(this.onTimerEvent.bind(this), + this.decay * this.frequency * 1000); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +document.getElementsByClassName = function(className, parentElement) { + var children = (document.body || $(parentElement)).getElementsByTagName('*'); + return $A(children).inject([], function(elements, child) { + if (Element.hasClassName(child, className)) + elements.push(child); + return elements; + }); +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Element) { + var Element = new Object(); +} + +Object.extend(Element, { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + Element[Element.visible(element) ? 'hide' : 'show'](element); + } + }, + + hide: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = 'none'; + } + }, + + show: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = ''; + } + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + }, + + getHeight: function(element) { + element = $(element); + return element.offsetHeight; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).include(className); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).add(className); + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).remove(className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + for (var i = 0; i < element.childNodes.length; i++) { + var node = element.childNodes[i]; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + Element.remove(node); + } + }, + + empty: function(element) { + return $(element).innerHTML.match(/^\s*$/); + }, + + scrollTo: function(element) { + element = $(element); + var x = element.x ? element.x : element.offsetLeft, + y = element.y ? element.y : element.offsetTop; + window.scrollTo(x, y); + }, + + getStyle: function(element, style) { + element = $(element); + var value = element.style[style.camelize()]; + if (!value) { + if (document.defaultView && document.defaultView.getComputedStyle) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css.getPropertyValue(style) : null; + } else if (element.currentStyle) { + value = element.currentStyle[style.camelize()]; + } + } + + if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) + if (Element.getStyle(element, 'position') == 'static') value = 'auto'; + + return value == 'auto' ? null : value; + }, + + getDimensions: function(element) { + element = $(element); + if (Element.getStyle(element, 'display') != 'none') + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = ''; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = 'none'; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (window.opera) { + element.style.top = 0; + element.style.left = 0; + } + } + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return; + element._overflow = element.style.overflow; + if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') + element.style.overflow = 'hidden'; + }, + + undoClipping: function(element) { + element = $(element); + if (element._overflow) return; + element.style.overflow = element._overflow; + element._overflow = undefined; + } +}); + +var Toggle = new Object(); +Toggle.display = Element.toggle; + +/*--------------------------------------------------------------------------*/ + +Abstract.Insertion = function(adjacency) { + this.adjacency = adjacency; +} + +Abstract.Insertion.prototype = { + initialize: function(element, content) { + this.element = $(element); + this.content = content; + + if (this.adjacency && this.element.insertAdjacentHTML) { + try { + this.element.insertAdjacentHTML(this.adjacency, this.content); + } catch (e) { + if (this.element.tagName.toLowerCase() == 'tbody') { + this.insertContent(this.contentFromAnonymousTable()); + } else { + throw e; + } + } + } else { + this.range = this.element.ownerDocument.createRange(); + if (this.initializeRange) this.initializeRange(); + this.insertContent([this.range.createContextualFragment(this.content)]); + } + }, + + contentFromAnonymousTable: function() { + var div = document.createElement('div'); + div.innerHTML = '' + this.content + '
    '; + return $A(div.childNodes[0].childNodes[0].childNodes); + } +} + +var Insertion = new Object(); + +Insertion.Before = Class.create(); +Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { + initializeRange: function() { + this.range.setStartBefore(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, this.element); + }).bind(this)); + } +}); + +Insertion.Top = Class.create(); +Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(true); + }, + + insertContent: function(fragments) { + fragments.reverse().each((function(fragment) { + this.element.insertBefore(fragment, this.element.firstChild); + }).bind(this)); + } +}); + +Insertion.Bottom = Class.create(); +Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.appendChild(fragment); + }).bind(this)); + } +}); + +Insertion.After = Class.create(); +Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { + initializeRange: function() { + this.range.setStartAfter(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, + this.element.nextSibling); + }).bind(this)); + } +}); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, + + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, + + set: function(className) { + this.element.className = className; + }, + + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set(this.toArray().concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set(this.select(function(className) { + return className != classNameToRemove; + })); + }, + + toString: function() { + return this.toArray().join(' '); + } +} + +Object.extend(Element.ClassNames.prototype, Enumerable); +var Field = { + clear: function() { + for (var i = 0; i < arguments.length; i++) + $(arguments[i]).value = ''; + }, + + focus: function(element) { + $(element).focus(); + }, + + present: function() { + for (var i = 0; i < arguments.length; i++) + if ($(arguments[i]).value == '') return false; + return true; + }, + + select: function(element) { + $(element).select(); + }, + + activate: function(element) { + $(element).focus(); + $(element).select(); + } +} + +/*--------------------------------------------------------------------------*/ + +var Form = { + serialize: function(form) { + var elements = Form.getElements($(form)); + var queryComponents = new Array(); + + for (var i = 0; i < elements.length; i++) { + var queryComponent = Form.Element.serialize(elements[i]); + if (queryComponent) + queryComponents.push(queryComponent); + } + + return queryComponents.join('&'); + }, + + getElements: function(form) { + var form = $(form); + var elements = new Array(); + + for (tagName in Form.Element.Serializers) { + var tagElements = form.getElementsByTagName(tagName); + for (var j = 0; j < tagElements.length; j++) + elements.push(tagElements[j]); + } + return elements; + }, + + getInputs: function(form, typeName, name) { + var form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) + return inputs; + + var matchingInputs = new Array(); + for (var i = 0; i < inputs.length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || + (name && input.name != name)) + continue; + matchingInputs.push(input); + } + + return matchingInputs; + }, + + disable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.blur(); + element.disabled = 'true'; + } + }, + + enable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.disabled = ''; + } + }, + + focusFirstElement: function(form) { + var form = $(form); + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + if (element.type != 'hidden' && !element.disabled) { + Field.activate(element); + break; + } + } + }, + + reset: function(form) { + $(form).reset(); + } +} + +Form.Element = { + serialize: function(element) { + var element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) + return encodeURIComponent(parameter[0]) + '=' + + encodeURIComponent(parameter[1]); + }, + + getValue: function(element) { + var element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) + return parameter[1]; + } +} + +Form.Element.Serializers = { + input: function(element) { + switch (element.type.toLowerCase()) { + case 'submit': + case 'hidden': + case 'password': + case 'text': + return Form.Element.Serializers.textarea(element); + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element); + } + return false; + }, + + inputSelector: function(element) { + if (element.checked) + return [element.name, element.value]; + }, + + textarea: function(element) { + return [element.name, element.value]; + }, + + select: function(element) { + return Form.Element.Serializers[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + + selectOne: function(element) { + var value = '', opt, index = element.selectedIndex; + if (index >= 0) { + opt = element.options[index]; + value = opt.value; + if (!value && !('value' in opt)) + value = opt.text; + } + return [element.name, value]; + }, + + selectMany: function(element) { + var value = new Array(); + for (var i = 0; i < element.length; i++) { + var opt = element.options[i]; + if (opt.selected) { + var optValue = opt.value; + if (!optValue && !('value' in opt)) + optValue = opt.text; + value.push(optValue); + } + } + return [element.name, value]; + } +} + +/*--------------------------------------------------------------------------*/ + +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = function() {} +Abstract.TimedObserver.prototype = { + initialize: function(element, frequency, callback) { + this.frequency = frequency; + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + } +} + +Form.Element.Observer = Class.create(); +Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(); +Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = function() {} +Abstract.EventObserver.prototype = { + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + var elements = Form.getElements(this.element); + for (var i = 0; i < elements.length; i++) + this.registerCallback(elements[i]); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + element.target = this; + element.prev_onclick = element.onclick || Prototype.emptyFunction; + element.onclick = function() { + this.prev_onclick(); + this.target.onElementEvent(); + } + break; + case 'password': + case 'text': + case 'textarea': + case 'select-one': + case 'select-multiple': + element.target = this; + element.prev_onchange = element.onchange || Prototype.emptyFunction; + element.onchange = function() { + this.prev_onchange(); + this.target.onElementEvent(); + } + break; + } + } + } +} + +Form.Element.EventObserver = Class.create(); +Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(); +Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) { + var Event = new Object(); +} + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + + element: function(event) { + return event.target || event.srcElement; + }, + + isLeftClick: function(event) { + return (((event.which) && (event.which == 1)) || + ((event.button) && (event.button == 1))); + }, + + pointerX: function(event) { + return event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)); + }, + + pointerY: function(event) { + return event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)); + }, + + stop: function(event) { + if (event.preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } else { + event.returnValue = false; + event.cancelBubble = true; + } + }, + + // find the first node with the given tagName, starting from the + // node the event was triggered on; traverses the DOM upwards + findElement: function(event, tagName) { + var element = Event.element(event); + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))) + element = element.parentNode; + return element; + }, + + observers: false, + + _observeAndCache: function(element, name, observer, useCapture) { + if (!this.observers) this.observers = []; + if (element.addEventListener) { + this.observers.push([element, name, observer, useCapture]); + element.addEventListener(name, observer, useCapture); + } else if (element.attachEvent) { + this.observers.push([element, name, observer, useCapture]); + element.attachEvent('on' + name, observer); + } + }, + + unloadCache: function() { + if (!Event.observers) return; + for (var i = 0; i < Event.observers.length; i++) { + Event.stopObserving.apply(this, Event.observers[i]); + Event.observers[i][0] = null; + } + Event.observers = false; + }, + + observe: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.attachEvent)) + name = 'keydown'; + + this._observeAndCache(element, name, observer, useCapture); + }, + + stopObserving: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.detachEvent)) + name = 'keydown'; + + if (element.removeEventListener) { + element.removeEventListener(name, observer, useCapture); + } else if (element.detachEvent) { + element.detachEvent('on' + name, observer); + } + } +}); + +/* prevent memory leaks in IE */ +Event.observe(window, 'unload', Event.unloadCache, false); +var Position = { + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + // must be called before calling withinIncludingScrolloffset, every time the + // page is scrolled + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + realOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return [valueL, valueT]; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return [valueL, valueT]; + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return [valueL, valueT]; + }, + + offsetParent: function(element) { + if (element.offsetParent) return element.offsetParent; + if (element == document.body) return element; + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return element; + + return document.body; + }, + + // caches x/y coordinate pair to use with overlap + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = this.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = this.realOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = this.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + // within must be called directly before + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + clone: function(source, target) { + source = $(source); + target = $(target); + target.style.position = 'absolute'; + var offsets = this.cumulativeOffset(source); + target.style.top = offsets[1] + 'px'; + target.style.left = offsets[0] + 'px'; + target.style.width = source.offsetWidth + 'px'; + target.style.height = source.offsetHeight + 'px'; + }, + + page: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent==document.body) + if (Element.getStyle(element,'position')=='absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } while (element = element.parentNode); + + return [valueL, valueT]; + }, + + clone: function(source, target) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || {}) + + // find page position of source + source = $(source); + var p = Position.page(source); + + // find coordinate system to use + target = $(target); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(target,'position') == 'absolute') { + parent = Position.offsetParent(target); + delta = Position.page(parent); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if(options.setWidth) target.style.width = source.offsetWidth + 'px'; + if(options.setHeight) target.style.height = source.offsetHeight + 'px'; + }, + + absolutize: function(element) { + element = $(element); + if (element.style.position == 'absolute') return; + Position.prepare(); + + var offsets = Position.positionedOffset(element); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px';; + element.style.left = left + 'px';; + element.style.width = width + 'px';; + element.style.height = height + 'px';; + }, + + relativize: function(element) { + element = $(element); + if (element.style.position == 'relative') return; + Position.prepare(); + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + } +} + +// Safari returns margins on body which is incorrect if the child is absolutely +// positioned. For performance reasons, redefine Position.cumulativeOffset for +// KHTML/WebKit only. +if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + Position.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return [valueL, valueT]; + } +} \ No newline at end of file diff --git a/public/javascripts/scriptaculous.js b/public/javascripts/scriptaculous.js new file mode 100644 index 00000000..cd0e3417 --- /dev/null +++ b/public/javascripts/scriptaculous.js @@ -0,0 +1,47 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +var Scriptaculous = { + Version: '1.5_rc3', + require: function(libraryName) { + // inserting via DOM fails in Safari 2.0, so brute force approach + document.write(''); + }, + load: function() { + if((typeof Prototype=='undefined') || + parseFloat(Prototype.Version.split(".")[0] + "." + + Prototype.Version.split(".")[1]) < 1.4) + throw("script.aculo.us requires the Prototype JavaScript framework >= 1.4.0"); + var scriptTags = document.getElementsByTagName("script"); + for(var i=0;i this.maximum) sliderValue = this.maximum; + if(sliderValue < this.minimum) sliderValue = this.minimum; + var offsetDiff = (sliderValue - (this.value||this.minimum)) * this.increment; + + if(this.isVertical()){ + this.setCurrentTop(offsetDiff + this.currentTop()); + } else { + this.setCurrentLeft(offsetDiff + this.currentLeft()); + } + this.value = sliderValue; + this.updateFinished(); + }, + minimumOffset: function(){ + return(this.isVertical() ? + this.trackTop() + this.alignY : + this.trackLeft() + this.alignX); + }, + maximumOffset: function(){ + return(this.isVertical() ? + this.trackTop() + this.alignY + (this.maximum - this.minimum) * this.increment : + this.trackLeft() + this.alignX + (this.maximum - this.minimum) * this.increment); + }, + isVertical: function(){ + return (this.axis == 'vertical'); + }, + startDrag: function(event) { + if(Event.isLeftClick(event)) { + if(!this.disabled){ + this.active = true; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var offsets = Position.cumulativeOffset(this.handle); + this.offsetX = (pointer[0] - offsets[0]); + this.offsetY = (pointer[1] - offsets[1]); + this.originalLeft = this.currentLeft(); + this.originalTop = this.currentTop(); + } + Event.stop(event); + } + }, + update: function(event) { + if(this.active) { + if(!this.dragging) { + var style = this.handle.style; + this.dragging = true; + if(style.position=="") style.position = "relative"; + style.zIndex = this.options.zindex; + } + this.draw(event); + // fix AppleWebKit rendering + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + Event.stop(event); + } + }, + draw: function(event) { + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var offsets = Position.cumulativeOffset(this.handle); + + offsets[0] -= this.currentLeft(); + offsets[1] -= this.currentTop(); + + // Adjust for the pointer's position on the handle + pointer[0] -= this.offsetX; + pointer[1] -= this.offsetY; + var style = this.handle.style; + + if(this.isVertical()){ + if(pointer[1] > this.maximumOffset()) + pointer[1] = this.maximumOffset(); + if(pointer[1] < this.minimumOffset()) + pointer[1] = this.minimumOffset(); + + // Increment by values + if(this.values){ + this.value = this.getNearestValue(Math.round((pointer[1] - this.minimumOffset()) / this.increment) + this.minimum); + pointer[1] = this.trackTop() + this.alignY + (this.value - this.minimum) * this.increment; + } else { + this.value = Math.round((pointer[1] - this.minimumOffset()) / this.increment) + this.minimum; + } + style.top = pointer[1] - offsets[1] + "px"; + } else { + if(pointer[0] > this.maximumOffset()) pointer[0] = this.maximumOffset(); + if(pointer[0] < this.minimumOffset()) pointer[0] = this.minimumOffset(); + // Increment by values + if(this.values){ + this.value = this.getNearestValue(Math.round((pointer[0] - this.minimumOffset()) / this.increment) + this.minimum); + pointer[0] = this.trackLeft() + this.alignX + (this.value - this.minimum) * this.increment; + } else { + this.value = Math.round((pointer[0] - this.minimumOffset()) / this.increment) + this.minimum; + } + style.left = (pointer[0] - offsets[0]) + "px"; + } + if(this.options.onSlide) this.options.onSlide(this.value); + }, + endDrag: function(event) { + if(this.active && this.dragging) { + this.finishDrag(event, true); + Event.stop(event); + } + this.active = false; + this.dragging = false; + }, + finishDrag: function(event, success) { + this.active = false; + this.dragging = false; + this.handle.style.zIndex = this.originalZ; + this.originalLeft = this.currentLeft(); + this.originalTop = this.currentTop(); + this.updateFinished(); + }, + updateFinished: function() { + if(this.options.onChange) this.options.onChange(this.value); + }, + keyPress: function(event) { + if(this.active && !this.disabled) { + switch(event.keyCode) { + case Event.KEY_ESC: + this.finishDrag(event, false); + Event.stop(event); + break; + } + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + } + } +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..4ab9e89f --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file \ No newline at end of file diff --git a/public/stylesheets/instiki.css b/public/stylesheets/instiki.css new file mode 100644 index 00000000..e676f3b9 --- /dev/null +++ b/public/stylesheets/instiki.css @@ -0,0 +1,320 @@ +body { +background-color:#FFF; +color:#333; +font-family:Verdana, Arial, Helvetica, sans-serif; +font-size:90%; +line-height:1.3em; +} + +#Container { +float:none; +margin:0 15%; +text-align:center; +} + +#Content { +border-top:none; +float:left; +margin:0; +padding:0.3em; +text-align:left; +width:100%; +} + +a:visited { +color:#666; +} + +h1,h2,h3 { +color:#333; +font-family:georgia, verdana, sans-serif; +} + +h1 { +font-size:200%; +} + +h2 { +font-size:130%; +} + +h3 { +font-size:120%; +} + +h1#pageName { +line-height:1em; +margin:0.2em 0 0; +padding:0; +} + +h1#pageName small { +color:#444; +font-size:35%; +line-height:1em; +padding:0; +} + +a.nav,a.nav:link,a.nav:visited { +background-color:#FFF; +color:#000; +} + +table { +border:double #000; +border-collapse:collapse; +} + +td { +border:thin solid #888; +} + +li { +margin-bottom:0.5em; +} + +.newWikiWord { +background-color:#DDD; +} + +.newWikiWord a:hover { +background-color:#FFF; +} + +form#navigationSearchForm { +display:inline; +} + +form#navigationSearchForm input { +font-size:80%; +} + +.navigation { +color:#999; +font-size:90%; +margin-top:0.3em; +} + +.navigation a:hover { +background-color:#000; +color:#FFF; +text-decoration:none; +} + +.navigation a { +color:#000; +font-weight:bold; +} + +.navigation small a { +font-size:90%; +font-weight:normal; +} + +.navOn { +color:#444; +font-weight:bold; +text-decoration:none; +} + +div.help { +font-family:verdana, arial, helvetica, sans-serif; +font-size:70%; +} + +div.inputBox { +background-color:#EEE; +font-family:verdana, arial, helvetica, sans-serif; +font-size:80%; +margin-bottom:1.5em; +padding:0.3em; +} + +blockquote { +display:block; +font-size:90%; +font-style:italic; +line-height:1.5em; +margin:0 0 1.5em; +padding:0 2.5em; +} + +pre { +background-color:#DDD; +font-size:90%; +overflow:auto; +padding:1em; +} + +ol.setup { +font-family:georgia, verdana, sans-serif; +font-size:110%; +margin-top:1em; +padding-left:1.5em; +} + +.byline { +color:#999; +font-size:65%; +font-style:italic; +margin-bottom:1em; +padding-top:1px; +} + +.diffdel,del.diffmod { +background-color:#FAA; +} + +.diffins,ins.diffmod { +background-color:#AFA; +} + +#footer { +color:#999; +font-size:60%; +font-style:italic; +line-height:1.2em; +padding-top:2em; +text-align:right; +} + +#footer a:link,#footer a:visited { +color:#888; +font-style:italic; +} + +div.web_normal { + padding:4px; +} + +div.web_protected { + padding:4px; + background-color:#DDD; +} + +div.inputFieldWithPrompt { +margin:0.75em 0; +} + +div.errorExplanation { +background-color:#FFA; +color:#900; +font-style:italic; +font-weight:bold; +margin:1.5em 0; +padding:1em; +width:100%; +} + +div.errorExplanation h2 { +display:none; +} + +div.errorExplanation ul { +border:none; +margin:0.5em 0 0 2em; +padding:0; +} + +div.fieldWithErrors input { +border:1px solid #900; +} + +div.info { +background-color:#DDD; +color:#060; +font-weight:bold; +margin-top:0.5em; +padding:0.5em; +width:100%; +} + +div#editFormButtons { +margin:0.5em 0 0; +} + +div#editFormButtons span { +white-space:nowrap; +} + +div#editForm textarea#content { +height:400px; +width:70%; +} + +div#MarkupHelp { +float:right; +margin-top:0.5em; +width:25%; +} + +div#MarkupHelp table { +border-bottom:3px solid #BBB; +border-left:3px solid #999; +border-right:3px solid #BBB; +border-top:3px solid #999; +margin-bottom:0; +} + +div#MarkupHelp td { +border:1px solid #999; +border-width:1px 0; +font-size:80%; +margin:0; +padding:0.2em; +vertical-align:top; +white-space:nowrap; +} + +div#MarkupHelp td.arrow { +color:#999; +padding:0 0.75em; +} + +div#MarkupHelp h3 { +font-size:90%; +font-weight:bold; +margin:0 0 5px; +padding:5px 0 0; +} + +div#MarkupHelp p { +font-size:70%; +} + +div.rightHandSide { +border-left:1px dotted #ccc; +float:right; +font-size:80%; +margin-left:0.7em; +padding-left:1.5em; +width:25%; +} + +.newsList { +margin-top:1.5em; +} + +.newsList p { +margin-bottom:2.5em; +} + +.property { +color:#999; +font-size:80%; +} + +a,li span { +color:#000; +} + +a:hover,a.nav:hover { +background-color:#000; +color:#FFF; +} + +div.errorExplanation p,div.errorExplanation li { +border:none; +margin:0; +padding:0; +} \ No newline at end of file diff --git a/rakefile.rb b/rakefile.rb new file mode 100755 index 00000000..cffd19f0 --- /dev/null +++ b/rakefile.rb @@ -0,0 +1,10 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/switchtower.rake, and they will automatically be available to Rake. + +require(File.join(File.dirname(__FILE__), 'config', 'boot')) + +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +require 'tasks/rails' \ No newline at end of file diff --git a/script/benchmarker b/script/benchmarker new file mode 100755 index 00000000..4a0ea231 --- /dev/null +++ b/script/benchmarker @@ -0,0 +1,19 @@ +#!/usr/bin/env ruby + +if ARGV.empty? + puts "Usage: benchmarker times 'Person.expensive_way' 'Person.another_expensive_way' ..." + exit +end + +require File.dirname(__FILE__) + '/../config/environment' +require 'benchmark' +include Benchmark + +# Don't include compilation in the benchmark +ARGV[1..-1].each { |expression| eval(expression) } + +bm(6) do |x| + ARGV[1..-1].each_with_index do |expression, idx| + x.report("##{idx + 1}") { ARGV[0].to_i.times { eval(expression) } } + end +end \ No newline at end of file diff --git a/script/breakpointer b/script/breakpointer new file mode 100755 index 00000000..6375616b --- /dev/null +++ b/script/breakpointer @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require 'rubygems' +require_gem 'rails' +require 'breakpoint_client' diff --git a/script/console b/script/console new file mode 100755 index 00000000..e23abda3 --- /dev/null +++ b/script/console @@ -0,0 +1,23 @@ +#!/usr/bin/env ruby +irb = RUBY_PLATFORM =~ /mswin32/ ? 'irb.bat' : 'irb' + +require 'optparse' +options = { :sandbox => false, :irb => irb } +OptionParser.new do |opt| + opt.on('-s', '--sandbox', 'Rollback database modifications on exit.') { |options[:sandbox]| } + opt.on("--irb=[#{irb}]", 'Invoke a different irb.') { |options[:irb]| } + opt.parse!(ARGV) +end + +libs = " -r irb/completion" +libs << " -r #{File.dirname(__FILE__)}/../config/environment" +libs << " -r console_sandbox" if options[:sandbox] + +ENV['RAILS_ENV'] = ARGV.first || 'development' +if options[:sandbox] + puts "Loading #{ENV['RAILS_ENV']} environment in sandbox." + puts "Any modifications you make will be rolled back on exit." +else + puts "Loading #{ENV['RAILS_ENV']} environment." +end +exec "#{options[:irb]} #{libs} --prompt-mode simple" diff --git a/script/destroy b/script/destroy new file mode 100755 index 00000000..46cc786e --- /dev/null +++ b/script/destroy @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/environment' +require 'rails_generator' +require 'rails_generator/scripts/destroy' + +ARGV.shift if ['--help', '-h'].include?(ARGV[0]) +Rails::Generator::Scripts::Destroy.new.run(ARGV) diff --git a/script/generate b/script/generate new file mode 100755 index 00000000..26447804 --- /dev/null +++ b/script/generate @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/environment' +require 'rails_generator' +require 'rails_generator/scripts/generate' + +ARGV.shift if ['--help', '-h'].include?(ARGV[0]) +Rails::Generator::Scripts::Generate.new.run(ARGV) diff --git a/script/import_storage b/script/import_storage new file mode 100755 index 00000000..7c53af9e --- /dev/null +++ b/script/import_storage @@ -0,0 +1,228 @@ +#!/usr/bin/env ruby + +require 'optparse' + +OPTIONS = { + :instiki_root => nil, + :storage => nil, + :database => 'mysql' +} + +ARGV.options do |opts| + script_name = File.basename($0) + opts.banner = "Usage: ruby #{script_name} [options]" + + opts.separator "" + + opts.on("-t", "--storage /full/path/to/storage", String, + "Full path to your storage, ", + "such as /home/joe/instiki/storage/2500", + "It should be the directory that ", + "contains .snapshot files.") do |storage| + OPTIONS[:storage] = storage + end + + opts.separator "" + + opts.on("-i", "--instiki /full/path/to/instiki", String, + "Full path to your Instiki 0.10 installation, ", + "such as /home/joe/instiki-0.10.2") do |instiki| + OPTIONS[:instiki] = instiki + end + + opts.separator "" + + opts.on("-o", "--outfile /full/path/to/output_file", String, + "Full path (including filename!) to where ", + "you want the SQL output placed, such as ", + "/home/joe/instiki.sql") do |outfile| + OPTIONS[:outfile] = outfile + end + + opts.on("-d", "--database {mysql|sqlite|postgres}", String, + "Target database (they have slightly different syntax)", + "default: mysql") do |database| + OPTIONS[:database] = database + end + + opts.separator "" + + opts.on_tail("-h", "--help", + "Show this help message.") { puts opts; exit } + + opts.parse! +end + +if OPTIONS[:instiki].nil? or OPTIONS[:storage].nil? or OPTIONS[:outfile].nil? + $stderr.puts "Please specify full paths to Instiki 0.10 installation and storage," + $stderr.puts "as well as the path to the output file" + $stderr.puts + puts ARGV.options + exit -1 +end + +if FileTest.exists? OPTIONS[:outfile] + $stderr.puts "Output file #{OPTIONS[:outfile]} already exists!" + $stderr.puts "Please specify a new file" + $stderr.puts + puts ARGV.options + exit -1 +end + +raise "Directory #{OPTIONS[:instiki]} not found" unless File.directory?(OPTIONS[:instiki]) +raise "Directory #{OPTIONS[:storage]} not found" unless File.directory?(OPTIONS[:storage]) + +expected_page_rb_path = File.join(OPTIONS[:instiki], 'app/models/page.rb') +raise "Instiki installation not found in #{OPTIONS[:instiki]}" unless File.file?(expected_page_rb_path) + +expected_snapshot_pattern = File.join(OPTIONS[:storage], '*.snapshot') +raise "No snapshots found in #{expected_snapshot_pattern}" if Dir[expected_snapshot_pattern].empty? + +INSTIKI_ROOT = File.expand_path(OPTIONS[:instiki]) + +ADDITIONAL_LOAD_PATHS = %w( + app/models + lib + vendor/madeleine-0.7.1/lib + vendor/RedCloth-3.0.3/lib + vendor/RedCloth-3.0.4/lib + vendor/rubyzip-0.5.8/lib +).map { |dir| "#{File.expand_path(File.join(INSTIKI_ROOT, dir))}" +}.delete_if { |dir| not File.exist?(dir) } + +# Prepend to $LOAD_PATH +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } + +require 'webrick' +require 'wiki_service' + +# substitute an extremely expensive method with something cheap. +class Revision + alias :__display_content :display_content + def display_content + return self + end +end + +class Time + def ansi + strftime('%Y-%m-%d %H:%M:%S') + end +end + +def sql_insert(table, hash) + columns = hash.keys + + values = columns.map { |column| hash[column] } + values = values.map do |value| + if value.nil? + 'NULL' + else + if (value == false or value == true) and OPTIONS[:database] == 'mysql' + value = value ? '1' : '0' + end + + case OPTIONS[:database] + when 'mysql', 'postgres' + value = value.to_s.gsub("'", "\\\\'") + when 'sqlite' + value = value.to_s.gsub("'", "''") + else + raise "Unsupported database option #{OPTIONS[:database]}" + end + "'#{value.gsub("\r\n", "\n")}'" + end + end + + output = "INSERT INTO #{table} (" + output << columns.join(", ") + + output << ") VALUES (" + output << values.join(", ") + output << ");" + output +end + +def delete_all(outfile) + %w(wiki_references revisions pages system webs).each { |table| outfile.puts "DELETE FROM #{table};" } +end + +def next_id(key) + $ids ||= {} + if $ids[key].nil? + $ids[key] = 1 + else + $ids[key] = $ids[key] + 1 + end + $ids[key] +end + +def current_id(key) + $ids[key] or raise "No curent ID for #{key.inspect}" +end + +WikiService.storage_path = OPTIONS[:storage] +wiki = WikiService.instance + +File.open(OPTIONS[:outfile], 'w') { |outfile| + + outfile.puts "BEGIN;" + delete_all(outfile) + outfile.puts "COMMIT;" + + wiki.webs.each_pair do |web_name, web| + outfile.puts "BEGIN;" + outfile.puts sql_insert(:webs, { + :id => next_id(:web), + :name => web.name, + :address => web.address, + :password => web.password, + :additional_style => web.additional_style, + :allow_uploads => web.allow_uploads, + :published => web.published, + :count_pages => web.count_pages, + :markup => web.markup, + :color => web.color, + :max_upload_size => web.max_upload_size, + :safe_mode => web.safe_mode, + :brackets_only => web.brackets_only, + :created_at => web.pages.values.map { |p| p.revisions.first.created_at }.min.ansi, + :updated_at => web.pages.values.map { |p| p.revisions.last.created_at }.max.ansi + }) + outfile.puts "COMMIT;" + + puts "Web #{web_name} has #{web.pages.keys.size} pages" + web.pages.each_pair do |page_name, page| + + outfile.puts "BEGIN;" + + outfile.puts sql_insert(:pages, { + :id => next_id(:page), + :web_id => current_id(:web), + :locked_by => page.locked_by, + :name => page.name, + :created_at => page.revisions.first.created_at.ansi, + :updated_at => page.revisions.last.created_at.ansi + }) + + puts " Page #{page_name} has #{page.revisions.size} revisions" + page.revisions.each_with_index do |rev, i| + + outfile.puts sql_insert(:revisions, { + :id => next_id(:revision), + :page_id => current_id(:page), + :content => rev.content, + :author => rev.author.to_s, + :ip => (rev.author.is_a?(Author) ? rev.author.ip : 'N/A'), + :created_at => rev.created_at.ansi, + :updated_at => rev.created_at.ansi, + :revised_at => rev.created_at.ansi + }) + puts " Revision #{i} created at #{rev.created_at.ansi}" + end + + outfile.puts "COMMIT;" + + end + end +} diff --git a/script/profiler b/script/profiler new file mode 100755 index 00000000..77c9fbef --- /dev/null +++ b/script/profiler @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby +if ARGV.empty? + $stderr.puts "Usage: profiler 'Person.expensive_method(10)' [times]" + exit(1) +end + +# Keep the expensive require out of the profile. +$stderr.puts 'Loading Rails...' +require File.dirname(__FILE__) + '/../config/environment' + +# Define a method to profile. +if ARGV[1] and ARGV[1].to_i > 1 + eval "def profile_me() #{ARGV[1]}.times { #{ARGV[0]} } end" +else + eval "def profile_me() #{ARGV[0]} end" +end + +# Use the ruby-prof extension if available. Fall back to stdlib profiler. +begin + require 'prof' + $stderr.puts 'Using the ruby-prof extension.' + Prof.clock_mode = Prof::GETTIMEOFDAY + Prof.start + profile_me + results = Prof.stop + require 'rubyprof_ext' + Prof.print_profile(results, $stderr) +rescue LoadError + $stderr.puts 'Using the standard Ruby profiler.' + Profiler__.start_profile + profile_me + Profiler__.stop_profile + Profiler__.print_profile($stderr) +end diff --git a/script/reset_references b/script/reset_references new file mode 100755 index 00000000..dd9ffb4a --- /dev/null +++ b/script/reset_references @@ -0,0 +1,28 @@ +#!/usr/bin/env ruby + +ENV['RAILS_ENV'] = ARGV.first || 'development' + +$stderr.puts "Loading Rails for #{ENV['RAILS_ENV']} environment..." +require File.dirname(__FILE__) + '/../config/environment' + +class StubUrlGenerator + def make_link(*args) + 'StubLink' + end +end + +PageRenderer.setup_url_generator(StubUrlGenerator.new) +WikiReference.delete_all + +Web.find_all.each do |web| + web.pages.find(:all, :order => 'name').each do |page| + $stderr.puts "Processing page '#{page.name}'" + begin + PageRenderer.new(page.current_revision).display_content(update_references = true) + rescue => e + puts e + puts e.backtrace + end + end +end + diff --git a/script/runner b/script/runner new file mode 100755 index 00000000..9c8bbb13 --- /dev/null +++ b/script/runner @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +require 'optparse' + +options = { :environment => "development" } + +ARGV.options do |opts| + script_name = File.basename($0) + opts.banner = "Usage: runner 'puts Person.find(1).name' [options]" + + opts.separator "" + + opts.on("-e", "--environment=name", String, + "Specifies the environment for the runner to operate under (test/development/production).", + "Default: development") { |options[:environment]| } + + opts.separator "" + + opts.on("-h", "--help", + "Show this help message.") { puts opts; exit } + + opts.parse! +end + +ENV["RAILS_ENV"] = options[:environment] + +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../config/environment' +eval(ARGV.first) \ No newline at end of file diff --git a/script/server b/script/server new file mode 100755 index 00000000..27a7989a --- /dev/null +++ b/script/server @@ -0,0 +1,49 @@ +#!/usr/bin/env ruby + +require 'webrick' +require 'optparse' + +OPTIONS = { + :port => 2500, + :ip => "0.0.0.0", + :environment => "production", + :server_root => File.expand_path(File.dirname(__FILE__) + "/../public/"), + :server_type => WEBrick::SimpleServer +} + +ARGV.options do |opts| + script_name = File.basename($0) + opts.banner = "Usage: ruby #{script_name} [options]" + + opts.separator "" + + opts.on("-p", "--port=port", Integer, + "Runs Instiki on the specified port.", + "Default: 2500") { |OPTIONS[:port]| } + opts.on("-b", "--binding=ip", String, + "Binds Instiki to the specified ip.", + "Default: 0.0.0.0") { |OPTIONS[:ip]| } + opts.on("-e", "--environment=name", String, + "Specifies the environment to run this server under (test/development/production).", + "Default: development") { |OPTIONS[:environment]| } + opts.on("-d", "--daemon", + "Make Instiki run as a Daemon (only works if fork is available -- meaning on *nix)." + ) { OPTIONS[:server_type] = WEBrick::Daemon } + + opts.separator "" + + opts.on("-h", "--help", + "Show this help message.") { puts opts; exit } + + opts.parse! +end + +ENV["RAILS_ENV"] = OPTIONS[:environment] +require File.dirname(__FILE__) + "/../config/environment" +require 'webrick_server' + +OPTIONS['working_directory'] = File.expand_path(RAILS_ROOT) + +puts "=> Instiki started on http://#{OPTIONS[:ip]}:#{OPTIONS[:port]}" +puts "=> Ctrl-C to shutdown; call with --help for options" if OPTIONS[:server_type] == WEBrick::SimpleServer +DispatchServlet.dispatch(OPTIONS) diff --git a/test/fixtures/exported_markup.zip b/test/fixtures/exported_markup.zip new file mode 100644 index 00000000..565834b0 Binary files /dev/null and b/test/fixtures/exported_markup.zip differ diff --git a/test/fixtures/pages.yml b/test/fixtures/pages.yml new file mode 100644 index 00000000..65fba487 --- /dev/null +++ b/test/fixtures/pages.yml @@ -0,0 +1,55 @@ +home_page: + id: 1 + created_at: <%= Time.local(2004, 4, 4, 16, 50).to_formatted_s(:db) %> + updated_at: <%= Time.local(2004, 4, 4, 16, 50).to_formatted_s(:db) %> + web_id: 1 + name: HomePage + +my_way: + id: 2 + created_at: <%= 9.days.ago.to_formatted_s(:db) %> + updated_at: <%= 9.days.ago.to_formatted_s(:db) %> + web_id: 1 + name: MyWay + +smart_engine: + id: 3 + created_at: <%= 8.days.ago.to_formatted_s(:db) %> + updated_at: <%= 8.days.ago.to_formatted_s(:db) %> + web_id: 1 + name: SmartEngine + +that_way: + id: 4 + created_at: <%= 7.days.ago.to_formatted_s(:db) %> + updated_at: <%= 7.days.ago.to_formatted_s(:db) %> + web_id: 1 + name: ThatWay + +no_wiki_word: + id: 5 + created_at: <%= 6.days.ago.to_formatted_s(:db) %> + updated_at: <%= 6.days.ago.to_formatted_s(:db) %> + web_id: 1 + name: NoWikiWord + +first_page: + id: 6 + created_at: <%= Time.local(2004, 4, 4, 16, 55).to_formatted_s(:db) %> + updated_at: <%= Time.local(2004, 4, 4, 16, 55).to_formatted_s(:db) %> + web_id: 1 + name: FirstPage + +oak: + id: 7 + created_at: <%= 5.days.ago.to_formatted_s(:db) %> + updated_at: <%= 5.days.ago.to_formatted_s(:db) %> + web_id: 1 + name: Oak + +elephant: + id: 8 + created_at: <%= 10.minutes.ago.to_formatted_s(:db) %> + updated_at: <%= 10.minutes.ago.to_formatted_s(:db) %> + web_id: 1 + name: Elephant \ No newline at end of file diff --git a/test/fixtures/rails.gif b/test/fixtures/rails.gif new file mode 100755 index 00000000..58960ee4 Binary files /dev/null and b/test/fixtures/rails.gif differ diff --git a/test/fixtures/revisions.yml b/test/fixtures/revisions.yml new file mode 100644 index 00000000..d65c2bd2 --- /dev/null +++ b/test/fixtures/revisions.yml @@ -0,0 +1,83 @@ +home_page_first_revision: + id: 1 + created_at: <%= Time.local(2004, 4, 4, 15, 50).to_formatted_s(:db) %> + updated_at: <%= Time.local(2004, 4, 4, 15, 50).to_formatted_s(:db) %> + revised_at: <%= Time.local(2004, 4, 4, 15, 50).to_formatted_s(:db) %> + page_id: 1 + content: First revision of the HomePage end + author: AnAuthor + ip: 127.0.0.1 + +my_way_first_revision: + id: 2 + created_at: <%= 9.days.ago.to_formatted_s(:db) %> + updated_at: <%= 9.days.ago.to_formatted_s(:db) %> + revised_at: <%= 9.days.ago.to_formatted_s(:db) %> + page_id: 2 + content: MyWay + author: Me + +smart_engine_first_revision: + id: 3 + created_at: <%= 8.days.ago.to_formatted_s(:db) %> + updated_at: <%= 8.days.ago.to_formatted_s(:db) %> + revised_at: <%= 8.days.ago.to_formatted_s(:db) %> + page_id: 3 + content: SmartEngine + author: Me + +that_way_first_revision: + id: 4 + created_at: <%= 7.days.ago.to_formatted_s(:db) %> + updated_at: <%= 7.days.ago.to_formatted_s(:db) %> + revised_at: <%= 7.days.ago.to_formatted_s(:db) %> + page_id: 4 + content: ThatWay + author: Me + +no_wiki_word_first_revision: + id: 5 + created_at: <%= 6.days.ago.to_formatted_s(:db) %> + updated_at: <%= 6.days.ago.to_formatted_s(:db) %> + revised_at: <%= 6.days.ago.to_formatted_s(:db) %> + page_id: 5 + content: hey you + author: Me + +home_page_second_revision: + id: 6 + created_at: <%= Time.local(2004, 4, 4, 16, 50).to_formatted_s(:db) %> + updated_at: <%= Time.local(2004, 4, 4, 16, 50).to_formatted_s(:db) %> + revised_at: <%= Time.local(2004, 4, 4, 16, 50).to_formatted_s(:db) %> + page_id: 1 + content: HisWay would be MyWay in kinda ThatWay in HisWay though MyWay \OverThere -- see SmartEngine in that SmartEngineGUI + author: DavidHeinemeierHansson + +first_page_first_revision: + id: 7 + created_at: <%= Time.local(2004, 4, 4, 16, 55).to_formatted_s(:db) %> + updated_at: <%= Time.local(2004, 4, 4, 16, 55).to_formatted_s(:db) %> + revised_at: <%= Time.local(2004, 4, 4, 16, 55).to_formatted_s(:db) %> + page_id: 6 + content: HisWay would be MyWay in kinda ThatWay in HisWay though MyWay \\OverThere -- see SmartEngine in that SmartEngineGUI + author: DavidHeinemeierHansson + +oak_first_revision: + id: 8 + created_at: <%= 5.days.ago.to_formatted_s(:db) %> + updated_at: <%= 5.days.ago.to_formatted_s(:db) %> + revised_at: <%= 5.days.ago.to_formatted_s(:db) %> + page_id: 7 + content: "All about oak.\ncategory: trees" + author: TreeHugger + ip: 127.0.0.2 + +elephant_first_revision: + id: 9 + created_at: <%= 10.minutes.ago.to_formatted_s(:db) %> + updated_at: <%= 10.minutes.ago.to_formatted_s(:db) %> + revised_at: <%= 10.minutes.ago.to_formatted_s(:db) %> + page_id: 8 + content: "All about elephants.\ncategory: animals" + author: Guest + ip: 127.0.0.2 diff --git a/test/fixtures/system.yml b/test/fixtures/system.yml new file mode 100644 index 00000000..1b17f2fc --- /dev/null +++ b/test/fixtures/system.yml @@ -0,0 +1,2 @@ +system: + password: test_password diff --git a/test/fixtures/webs.yml b/test/fixtures/webs.yml new file mode 100644 index 00000000..05437295 --- /dev/null +++ b/test/fixtures/webs.yml @@ -0,0 +1,15 @@ +test_wiki: + id: 1 + created_at: 2004-08-01 + updated_at: 2005-08-01 + name: wiki1 + address: wiki1 + markup: textile + +instiki: + id: 2 + created_at: 2004-08-01 + updated_at: 2005-08-01 + name: Instiki + address: instiki + markup: textile \ No newline at end of file diff --git a/test/fixtures/wiki_references.yml b/test/fixtures/wiki_references.yml new file mode 100644 index 00000000..542a3013 --- /dev/null +++ b/test/fixtures/wiki_references.yml @@ -0,0 +1,112 @@ +my_way_1: + id: 1 + page_id: 2 + referenced_name: MyWay + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +smart_engine_1: + id: 2 + page_id: 3 + referenced_name: SmartEngine + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +that_way_1: + id: 3 + page_id: 4 + referenced_name: ThatWay + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +home_page_1: + id: 4 + page_id: 1 + referenced_name: HisWay + link_type: W + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +home_page_2: + id: 5 + page_id: 1 + referenced_name: MyWay + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +home_page_3: + id: 6 + page_id: 1 + referenced_name: ThatWay + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +home_page_4: + id: 7 + page_id: 1 + referenced_name: SmartEngine + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +first_page_1: + id: 8 + page_id: 6 + referenced_name: HisWay + link_type: W + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +first_page_2: + id: 9 + page_id: 6 + referenced_name: MyWay + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +first_page_3: + id: 10 + page_id: 6 + referenced_name: ThatWay + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +first_page_4: + id: 11 + page_id: 6 + referenced_name: OverThere + link_type: W + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +first_page_5: + id: 12 + page_id: 6 + referenced_name: SmartEngine + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +oak_1: + id: 13 + page_id: 7 + referenced_name: trees + link_type: C + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +elephant_1: + id: 14 + page_id: 8 + referenced_name: animals + link_type: C + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + \ No newline at end of file diff --git a/test/functional/admin_controller_test.rb b/test/functional/admin_controller_test.rb new file mode 100644 index 00000000..6df919f3 --- /dev/null +++ b/test/functional/admin_controller_test.rb @@ -0,0 +1,234 @@ +#!/usr/bin/env ruby + +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') +require 'admin_controller' + +# Raise errors beyond the default web-based presentation +class AdminController; def rescue_action(e) logger.error(e); raise e end; end + +class AdminControllerTest < Test::Unit::TestCase + fixtures :webs, :pages, :revisions, :system, :wiki_references + + def setup + @controller = AdminController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @wiki = Wiki.new + @oak = pages(:oak) + @elephant = pages(:elephant) + @web = webs(:test_wiki) + @home = @page = pages(:home_page) + end + + def test_create_system_form_displayed + use_blank_wiki + process('create_system') + assert_response :success + end + + def test_create_system_form_submitted + use_blank_wiki + assert !@wiki.setup? + + process('create_system', 'password' => 'a_password', 'web_name' => 'My Wiki', + 'web_address' => 'my_wiki') + + assert_redirected_to :web => 'my_wiki', :controller => 'wiki', :action => 'new', + :id => 'HomePage' + assert @wiki.setup? + assert_equal 'a_password', @wiki.system[:password] + assert_equal 1, @wiki.webs.size + new_web = @wiki.webs['my_wiki'] + assert_equal 'My Wiki', new_web.name + assert_equal 'my_wiki', new_web.address + end + + def test_create_system_form_submitted_and_wiki_already_initialized + wiki_before = @wiki + old_size = @wiki.webs.size + assert @wiki.setup? + + process 'create_system', 'password' => 'a_password', 'web_name' => 'My Wiki', + 'web_address' => 'my_wiki' + + assert_redirected_to :web => @wiki.webs.keys.first, :action => 'show', :id => 'HomePage' + assert_equal wiki_before, @wiki + # and no new web should be created either + assert_equal old_size, @wiki.webs.size + assert_flash_has :error + end + + def test_create_system_no_form_and_wiki_already_initialized + assert @wiki.setup? + process('create_system') + assert_redirected_to :web => @wiki.webs.keys.first, :action => 'show', :id => 'HomePage' + assert_flash_has :error + end + + + def test_create_web + @wiki.system.update_attribute(:password, 'pswd') + + process 'create_web', 'system_password' => 'pswd', 'name' => 'Wiki Two', 'address' => 'wiki2' + + assert_redirected_to :web => 'wiki2', :action => 'new', :id => 'HomePage' + wiki2 = @wiki.webs['wiki2'] + assert wiki2 + assert_equal 'Wiki Two', wiki2.name + assert_equal 'wiki2', wiki2.address + end + + def test_create_web_default_password + @wiki.system.update_attribute(:password, nil) + + process 'create_web', 'system_password' => 'instiki', 'name' => 'Wiki Two', 'address' => 'wiki2' + + assert_redirected_to :web => 'wiki2', :action => 'new', :id => 'HomePage' + end + + def test_create_web_failed_authentication + @wiki.system.update_attribute(:password, 'pswd') + + process 'create_web', 'system_password' => 'wrong', 'name' => 'Wiki Two', 'address' => 'wiki2' + + assert_redirected_to :web => nil, :action => 'index' + assert_nil @wiki.webs['wiki2'] + end + + def test_create_web_no_form_submitted + @wiki.system.update_attribute(:password, 'pswd') + process 'create_web' + assert_response :success + end + + + def test_edit_web_no_form + process 'edit_web', 'web' => 'wiki1' + # this action simply renders a form + assert_response :success + end + + def test_edit_web_form_submitted + @wiki.system.update_attribute(:password, 'pswd') + + process('edit_web', 'system_password' => 'pswd', + 'web' => 'wiki1', 'address' => 'renamed_wiki1', 'name' => 'Renamed Wiki1', + 'markup' => 'markdown', 'color' => 'blue', 'additional_style' => 'whatever', + 'safe_mode' => 'on', 'password' => 'new_password', 'published' => 'on', + 'brackets_only' => 'on', 'count_pages' => 'on', 'allow_uploads' => 'on', + 'max_upload_size' => '300') + + assert_redirected_to :web => 'renamed_wiki1', :action => 'show', :id => 'HomePage' + @web = Web.find(@web.id) + assert_equal 'renamed_wiki1', @web.address + assert_equal 'Renamed Wiki1', @web.name + assert_equal :markdown, @web.markup + assert_equal 'blue', @web.color + assert @web.safe_mode? + assert_equal 'new_password', @web.password + assert @web.published? + assert @web.brackets_only? + assert @web.count_pages? + assert @web.allow_uploads? + assert_equal 300, @web.max_upload_size + end + + def test_edit_web_opposite_values + @wiki.system.update_attribute(:password, 'pswd') + + process('edit_web', 'system_password' => 'pswd', + 'web' => 'wiki1', 'address' => 'renamed_wiki1', 'name' => 'Renamed Wiki1', + 'markup' => 'markdown', 'color' => 'blue', 'additional_style' => 'whatever', + 'password' => 'new_password') + # safe_mode, published, brackets_only, count_pages, allow_uploads not set + # and should become false + + assert_redirected_to :web => 'renamed_wiki1', :action => 'show', :id => 'HomePage' + @web = Web.find(@web.id) + assert !@web.safe_mode? + assert !@web.published? + assert !@web.brackets_only? + assert !@web.count_pages? + assert !@web.allow_uploads? + end + + def test_edit_web_wrong_password + process('edit_web', 'system_password' => 'wrong', + 'web' => 'wiki1', 'address' => 'renamed_wiki1', 'name' => 'Renamed Wiki1', + 'markup' => 'markdown', 'color' => 'blue', 'additional_style' => 'whatever', + 'password' => 'new_password') + + #returns to the same form + assert_response :success + assert @response.has_template_object?('error') + end + + def test_edit_web_rename_to_already_existing_web_name + @wiki.system.update_attribute(:password, 'pswd') + + @wiki.create_web('Another', 'another') + process('edit_web', 'system_password' => 'pswd', + 'web' => 'wiki1', 'address' => 'another', 'name' => 'Renamed Wiki1', + 'markup' => 'markdown', 'color' => 'blue', 'additional_style' => 'whatever', + 'password' => 'new_password') + + #returns to the same form + assert_response :success + assert @response.has_template_object?('error') + end + + def test_edit_web_empty_password + process('edit_web', 'system_password' => '', + 'web' => 'wiki1', 'address' => 'renamed_wiki1', 'name' => 'Renamed Wiki1', + 'markup' => 'markdown', 'color' => 'blue', 'additional_style' => 'whatever', + 'password' => 'new_password') + + #returns to the same form + assert_response :success + assert @response.has_template_object?('error') + end + + + def test_remove_orphaned_pages + @wiki.system.update_attribute(:password, 'pswd') + page_order = [@home, pages(:my_way), @oak, pages(:smart_engine), pages(:that_way)] + orphan_page_linking_to_oak = @wiki.write_page('wiki1', 'Pine', + "Refers to [[Oak]].\n" + + "category: trees", + Time.now, Author.new('TreeHugger', '127.0.0.2'), test_renderer) + + r = process('remove_orphaned_pages', 'web' => 'wiki1', 'system_password_orphaned' => 'pswd') + + assert_redirected_to :controller => 'wiki', :web => 'wiki1', :action => 'list' + @web.pages(true) + assert_equal page_order, @web.select.sort, + "Pages are not as expected: #{@web.select.sort.map {|p| p.name}.inspect}" + + # Oak is now orphan, second pass should remove it + r = process('remove_orphaned_pages', 'web' => 'wiki1', 'system_password_orphaned' => 'pswd') + assert_redirected_to :controller => 'wiki', :web => 'wiki1', :action => 'list' + @web.pages(true) + page_order.delete(@oak) + assert_equal page_order, @web.select.sort, + "Pages are not as expected: #{@web.select.sort.map {|p| p.name}.inspect}" + + # third pass does not destroy HomePage + r = process('remove_orphaned_pages', 'web' => 'wiki1', 'system_password_orphaned' => 'pswd') + assert_redirected_to :action => 'list' + @web.pages(true) + assert_equal page_order, @web.select.sort, + "Pages are not as expected: #{@web.select.sort.map {|p| p.name}.inspect}" + end + + def test_remove_orphaned_pages_empty_or_wrong_password + @wiki.system[:password] = 'pswd' + + process('remove_orphaned_pages', 'web' => 'wiki1') + assert_redirected_to(:controller => 'admin', :action => 'edit_web', :web => 'wiki1') + assert @response.flash[:error] + + process('remove_orphaned_pages', 'web' => 'wiki1', 'system_password_orphaned' => 'wrong') + assert_redirected_to(:controller => 'admin', :action => 'edit_web', :web => 'wiki1') + assert @response.flash[:error] + end +end diff --git a/test/functional/application_test.rb b/test/functional/application_test.rb new file mode 100755 index 00000000..c32f8b23 --- /dev/null +++ b/test/functional/application_test.rb @@ -0,0 +1,30 @@ +# Unit tests for ApplicationController (the abstract controller class) + +require File.dirname(__FILE__) + '/../test_helper' +require 'wiki_controller' +require 'rexml/document' + +# Need some concrete class to test the abstract class features +class WikiController; def rescue_action(e) logger.error(e); raise e end; end + +class ApplicationTest < Test::Unit::TestCase + fixtures :webs, :pages, :revisions, :system + + def setup + @controller = WikiController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @wiki = Wiki.new + end + + def test_utf8_header + get :show, :web => 'wiki1', :id => 'HomePage' + assert_equal 'text/html; charset=UTF-8', @response.headers['Content-Type'] + end + + def test_connect_to_model_unknown_wiki + get :show, :web => 'unknown_wiki', :id => 'HomePage' + assert_response :missing + end + +end diff --git a/test/functional/file_controller_test.rb b/test/functional/file_controller_test.rb new file mode 100755 index 00000000..98288864 --- /dev/null +++ b/test/functional/file_controller_test.rb @@ -0,0 +1,114 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../test_helper' +require 'file_controller' +require 'fileutils' +require 'stringio' + +# Raise errors beyond the default web-based presentation +class FileController; def rescue_action(e) logger.error(e); raise e end; end + +class FileControllerTest < Test::Unit::TestCase + fixtures :webs, :pages, :revisions, :system + + def setup + @controller = FileController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @web = webs(:test_wiki) + @wiki = Wiki.new + WikiFile.delete_all + require 'fileutils' + FileUtils.rm_rf("#{RAILS_ROOT}/public/wiki1/files/*") + end + + def test_file_upload_form + get :file, :web => 'wiki1', :id => 'new_file.txt' + assert_success + assert_rendered_file 'file/file' + end + + def test_file_download_text_file + @web.wiki_files.create(:file_name => 'foo.txt', :description => 'Text file', + :content => "Contents of the file") + + r = get :file, :web => 'wiki1', :id => 'foo.txt' + + assert_success(bypass_body_parsing = true) + assert_equal "Contents of the file", r.body + assert_equal 'text/plain', r.headers['Content-Type'] + end + + def test_file_download_pdf_file + @web.wiki_files.create(:file_name => 'foo.pdf', :description => 'PDF file', + :content => "aaa\nbbb\n") + + r = get :file, :web => 'wiki1', :id => 'foo.pdf' + + assert_success(bypass_body_parsing = true) + assert_equal "aaa\nbbb\n", r.body + assert_equal 'application/pdf', r.headers['Content-Type'] + end + + def test_pic_download_gif + pic = File.open("#{RAILS_ROOT}/test/fixtures/rails.gif", 'rb') { |f| f.read } + @web.wiki_files.create(:file_name => 'rails.gif', :description => 'An image', :content => pic) + + r = get :file, :web => 'wiki1', :id => 'rails.gif' + + assert_success(bypass_body_parsing = true) + assert_equal 'image/gif', r.headers['Content-Type'] + assert_equal pic.size, r.body.size + assert_equal pic, r.body + end + + def test_pic_unknown_pic + r = get :file, :web => 'wiki1', :id => 'non-existant.gif' + + assert_success + assert_rendered_file 'file/file' + end + + def test_pic_upload_end_to_end + # edit and re-render home page so that it has an "unknown file" link to 'rails-e2e.gif' + PageRenderer.setup_url_generator(StubUrlGenerator.new) + renderer = PageRenderer.new + @wiki.revise_page('wiki1', 'HomePage', '[[rails-e2e.gif:pic]]', + Time.now, 'AnonymousBrave', renderer) + assert_equal "

    rails-e2e.gif" + + "?

    ", + renderer.display_content + + # rails-e2e.gif is unknown to the system, so pic action goes to the file [upload] form + r = get :file, :web => 'wiki1', :id => 'rails-e2e.gif' + assert_success + assert_rendered_file 'file/file' + + # User uploads the picture + picture = File.read("#{RAILS_ROOT}/test/fixtures/rails.gif") + r = post :file, :web => 'wiki1', + :file => {:file_name => 'rails-e2e.gif', :content => StringIO.new(picture)} + assert_redirected_to({}) + assert @web.has_file?('rails-e2e.gif') + assert_equal(picture, WikiFile.find_by_file_name('rails-e2e.gif').content) + end + + def test_import + r = post :import, :web => 'wiki1', :file => uploaded_file("#{RAILS_ROOT}/test/fixtures/exported_markup.zip") + assert_redirect + assert @web.has_page?('ImportedPage') + end + + def uploaded_file(path, content_type="application/octet-stream", filename=nil) + filename ||= File.basename(path) + t = Tempfile.new(filename) + FileUtils.copy_file(path, t.path) + (class << t; self; end;).class_eval do + alias local_path path + define_method(:original_filename) { filename } + define_method(:content_type) { content_type } + end + return t + end + +end diff --git a/test/functional/routes_test.rb b/test/functional/routes_test.rb new file mode 100644 index 00000000..4ba2cec5 --- /dev/null +++ b/test/functional/routes_test.rb @@ -0,0 +1,51 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../test_helper' + +require 'action_controller/routing' + +class RoutesTest < Test::Unit::TestCase + + def test_parse_uri + assert_routing('', :controller => 'wiki', :action => 'index') + assert_routing('x', :controller => 'wiki', :action => 'index', :web => 'x') + assert_routing('x/y', :controller => 'wiki', :web => 'x', :action => 'y') + assert_routing('x/y/z', :controller => 'wiki', :web => 'x', :action => 'y', :id => 'z') + assert_recognizes({:web => 'x', :controller => 'wiki', :action => 'y'}, 'x/y/') + assert_recognizes({:web => 'x', :controller => 'wiki', :action => 'y', :id => 'z'}, 'x/y/z/') + end + + def test_parse_uri_interestng_cases + assert_routing('_veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeery-long_web_/an_action/HomePage', + :web => '_veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeery-long_web_', + :controller => 'wiki', + :action => 'an_action', :id => 'HomePage' + ) + assert_recognizes({:controller => 'wiki', :action => 'index'}, '///') + end + + def test_parse_uri_liberal_with_pagenames + + assert_routing('web/show/%24HOME_PAGE', + :controller => 'wiki', :web => 'web', :action => 'show', :id => '$HOME_PAGE') + + assert_routing('web/show/HomePage%3Farg1%3Dvalue1%26arg2%3Dvalue2', + :controller => 'wiki', :web => 'web', :action => 'show', + :id => 'HomePage?arg1=value1&arg2=value2') + + assert_routing('web/files/abc.zip', + :web => 'web', :controller => 'file', :action => 'file', :id => 'abc.zip') + assert_routing('web/import', :web => 'web', :controller => 'file', :action => 'import') + # default option is wiki + assert_recognizes({:controller => 'wiki', :web => 'unknown_path', :action => 'index', }, + 'unknown_path') + end + + def test_cases_broken_by_routes +# assert_routing('web/show/Page+With+Spaces', +# :controller => 'wiki', :web => 'web', :action => 'show', :id => 'Page With Spaces') +# assert_routing('web/show/HomePage%2Fsomething_else', +# :controller => 'wiki', :web => 'web', :action => 'show', :id => 'HomePage/something_else') + end + +end diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb new file mode 100755 index 00000000..3edf75b4 --- /dev/null +++ b/test/functional/wiki_controller_test.rb @@ -0,0 +1,676 @@ +#!/usr/bin/env ruby + +# Uncomment the line below to enable pdflatex tests; don't forget to comment them again +# commiting to SVN +# $INSTIKI_TEST_PDFLATEX = true + +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') +require 'wiki_controller' +require 'rexml/document' +require 'tempfile' +require 'zip/zipfilesystem' + +# Raise errors beyond the default web-based presentation +class WikiController; def rescue_action(e) logger.error(e); raise e end; end + +class WikiControllerTest < Test::Unit::TestCase + fixtures :webs, :pages, :revisions, :system, :wiki_references + + def setup + @controller = WikiController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @wiki = Wiki.new + @web = webs(:test_wiki) + @home = @page = pages(:home_page) + @oak = pages(:oak) + @elephant = pages(:elephant) + end + + def test_authenticate + set_web_property :password, 'pswd' + + get :authenticate, :web => 'wiki1', :password => 'pswd' + assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'HomePage' + assert_equal ['pswd'], @response.cookies['web_address'] + end + + def test_authenticate_wrong_password + set_web_property :password, 'pswd' + + r = process('authenticate', 'web' => 'wiki1', 'password' => 'wrong password') + assert_redirected_to :action => 'login', :web => 'wiki1' + assert_nil r.cookies['web_address'] + end + + def test_authors + @wiki.write_page('wiki1', 'BreakSortingOrder', + "This page breaks the accidentally correct sorting order of authors", + Time.now, Author.new('BreakingTheOrder', '127.0.0.2'), test_renderer) + + r = process('authors', 'web' => 'wiki1') + + assert_success + assert_equal %w(AnAuthor BreakingTheOrder DavidHeinemeierHansson Guest Me TreeHugger), + r.template_objects['authors'] + page_names_by_author = r.template_objects['page_names_by_author'] + assert_equal r.template_objects['authors'], page_names_by_author.keys.sort + assert_equal %w(FirstPage HomePage), page_names_by_author['DavidHeinemeierHansson'] + end + + def test_cancel_edit + @oak.lock(Time.now, 'Locky') + assert @oak.locked?(Time.now) + + r = process('cancel_edit', 'web' => 'wiki1', 'id' => 'Oak') + + assert_redirected_to :action => 'show', :id => 'Oak' + assert !Page.find(@oak.id).locked?(Time.now) + end + + def test_edit + r = process 'edit', 'web' => 'wiki1', 'id' => 'HomePage' + assert_success + assert_equal @wiki.read_page('wiki1', 'HomePage'), r.template_objects['page'] + end + + def test_edit_page_locked_page + @home.lock(Time.now, 'Locky') + process 'edit', 'web' => 'wiki1', 'id' => 'HomePage' + assert_redirected_to :action => 'locked' + end + + def test_edit_page_break_lock + @home.lock(Time.now, 'Locky') + process 'edit', 'web' => 'wiki1', 'id' => 'HomePage', 'break_lock' => 'y' + assert_success + @home = Page.find(@home.id) + assert @home.locked?(Time.now) + end + + def test_edit_unknown_page + process 'edit', 'web' => 'wiki1', 'id' => 'UnknownPage', 'break_lock' => 'y' + assert_redirected_to :controller => 'wiki', :action => 'show', :web => 'wiki1', + :id => 'HomePage' + end + + def test_edit_page_with_special_symbols + @wiki.write_page('wiki1', 'With : Special /> symbols', + 'This page has special symbols in the name', Time.now, Author.new('Special', '127.0.0.3'), + test_renderer) + + r = process 'edit', 'web' => 'wiki1', 'id' => 'With : Special /> symbols' + assert_success + xml = REXML::Document.new(r.body) + form = REXML::XPath.first(xml, '//form') + assert_equal '/wiki1/save/With+%3A+Special+%2F%3E+symbols', form.attributes['action'] + end + + def test_export_html + # rollback homepage to a version that is easier to match + @home.rollback(0, Time.now, 'Rick', test_renderer) + r = process 'export_html', 'web' => 'wiki1' + + assert_success(bypass_body_parsing = true) + assert_equal 'application/zip', r.headers['Content-Type'] + assert_match /attachment; filename="wiki1-html-\d\d\d\d-\d\d-\d\d-\d\d-\d\d-\d\d.zip"/, + r.headers['Content-Disposition'] + assert_equal 'PK', r.body[0..1], 'Content is not a zip file' + + # Tempfile doesn't know how to open files with binary flag, hence the two-step process + Tempfile.open('instiki_export_file') { |f| @tempfile_path = f.path } + begin + File.open(@tempfile_path, 'wb') { |f| f.write(r.body); @exported_file = f.path } + Zip::ZipFile.open(@exported_file) do |zip| + assert_equal %w(Elephant.html FirstPage.html HomePage.html MyWay.html NoWikiWord.html Oak.html SmartEngine.html ThatWay.html index.html), zip.dir.entries('.').sort + assert_match /.*/, + zip.file.read('Elephant.html').gsub(/\s+/, ' ') + assert_match /.*/, + zip.file.read('Oak.html').gsub(/\s+/, ' ') + assert_match /.*/, + zip.file.read('HomePage.html').gsub(/\s+/, ' ') + assert_equal ' ', zip.file.read('index.html').gsub(/\s+/, ' ') + end + ensure + File.delete(@tempfile_path) if File.exist?(@tempfile_path) + end + end + + def test_export_html_no_layout + r = process 'export_html', 'web' => 'wiki1', 'layout' => 'no' + + assert_success(bypass_body_parsing = true) + assert_equal 'application/zip', r.headers['Content-Type'] + assert_match /attachment; filename="wiki1-html-\d\d\d\d-\d\d-\d\d-\d\d-\d\d-\d\d.zip"/, + r.headers['Content-Disposition'] + assert_equal 'PK', r.body[0..1], 'Content is not a zip file' + end + + def test_export_markup + r = process 'export_markup', 'web' => 'wiki1' + + assert_success(bypass_body_parsing = true) + assert_equal 'application/zip', r.headers['Content-Type'] + assert_match /attachment; filename="wiki1-textile-\d\d\d\d-\d\d-\d\d-\d\d-\d\d-\d\d.zip"/, + r.headers['Content-Disposition'] + assert_equal 'PK', r.body[0..1], 'Content is not a zip file' + end + + + if ENV['INSTIKI_TEST_LATEX'] or defined? $INSTIKI_TEST_PDFLATEX + + def test_export_pdf + r = process 'export_pdf', 'web' => 'wiki1' + assert_success(bypass_body_parsing = true) + assert_equal 'application/pdf', r.headers['Content-Type'] + assert_match /attachment; filename="wiki1-tex-\d\d\d\d-\d\d-\d\d-\d\d-\d\d-\d\d.pdf"/, + r.headers['Content-Disposition'] + assert_equal '%PDF', r.body[0..3] + assert_equal "EOF\n", r.body[-4..-1] + end + + else + puts 'Warning: tests involving pdflatex are very slow, therefore they are disabled by default.' + puts ' Set environment variable INSTIKI_TEST_PDFLATEX or global Ruby variable' + puts ' $INSTIKI_TEST_PDFLATEX to enable them.' + end + + def test_export_tex + r = process 'export_tex', 'web' => 'wiki1' + + assert_success(bypass_body_parsing = true) + assert_equal 'application/octet-stream', r.headers['Content-Type'] + assert_match /attachment; filename="wiki1-tex-\d\d\d\d-\d\d-\d\d-\d\d-\d\d-\d\d.tex"/, + r.headers['Content-Disposition'] + assert_equal '\documentclass', r.body[0..13], 'Content is not a TeX file' + end + + def test_feeds + process('feeds', 'web' => 'wiki1') + end + + def test_index + # delete extra web fixture + webs(:instiki).destroy + process('index') + assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'HomePage' + end + + def test_index_multiple_webs + @wiki.create_web('Test Wiki 2', 'wiki2') + process('index') + assert_redirected_to :action => 'web_list' + end + + def test_index_multiple_webs_web_explicit + @wiki.create_web('Test Wiki 2', 'wiki2') + process('index', 'web' => 'wiki2') + assert_redirected_to :web => 'wiki2', :action => 'show', :id => 'HomePage' + end + + def test_index_wiki_not_initialized + use_blank_wiki + process('index') + assert_redirected_to :controller => 'admin', :action => 'create_system' + end + + + def test_list + r = process('list', 'web' => 'wiki1') + + assert_equal ['animals', 'trees'], r.template_objects['categories'] + assert_nil r.template_objects['category'] + assert_equal [@elephant, pages(:first_page), @home, pages(:my_way), pages(:no_wiki_word), + @oak, pages(:smart_engine), pages(:that_way)], + r.template_objects['pages_in_category'] + end + + + def test_locked + @home.lock(Time.now, 'Locky') + r = process('locked', 'web' => 'wiki1', 'id' => 'HomePage') + assert_success + assert_equal @home, r.template_objects['page'] + end + + + def test_login + r = process 'login', 'web' => 'wiki1' + assert_success + # this action goes straight to the templates + end + + + def test_new + r = process('new', 'id' => 'NewPage', 'web' => 'wiki1') + assert_success + assert_equal 'AnonymousCoward', r.template_objects['author'] + assert_equal 'NewPage', r.template_objects['page_name'] + end + + + if ENV['INSTIKI_TEST_LATEX'] or defined? $INSTIKI_TEST_PDFLATEX + + def test_pdf + assert RedClothForTex.available?, 'Cannot do test_pdf when pdflatex is not available' + r = process('pdf', 'web' => 'wiki1', 'id' => 'HomePage') + assert_success(bypass_body_parsing = true) + + assert_equal '%PDF', r.body[0..3] + assert_equal "EOF\n", r.body[-4..-1] + + assert_equal 'application/pdf', r.headers['Content-Type'] + assert_match /attachment; filename="HomePage-wiki1-\d\d\d\d-\d\d-\d\d-\d\d-\d\d-\d\d.pdf"/, + r.headers['Content-Disposition'] + end + + end + + + def test_print + r = process('print', 'web' => 'wiki1', 'id' => 'HomePage') + + assert_success + assert_equal :show, r.template_objects['link_mode'] + end + + + def test_published + set_web_property :published, true + + r = process('published', 'web' => 'wiki1', 'id' => 'HomePage') + + assert_success + assert_equal @home, r.template_objects['page'] + end + + + def test_published_web_not_published + set_web_property :published, false + + r = process('published', 'web' => 'wiki1', 'id' => 'HomePage') + + assert_response :missing + end + + def test_published_should_render_homepage_if_no_page_specified + set_web_property :published, true + + r = process('published', 'web' => 'wiki1') + + assert_success + assert_equal @home, r.template_objects['page'] + end + + + def test_recently_revised + r = process('recently_revised', 'web' => 'wiki1') + assert_success + + assert_equal %w(animals trees), r.template_objects['categories'] + assert_nil r.template_objects['category'] + all_pages = @elephant, pages(:first_page), @home, pages(:my_way), pages(:no_wiki_word), + @oak, pages(:smart_engine), pages(:that_way) + assert_equal all_pages, r.template_objects['pages_in_category'] + + pages_by_day = r.template_objects['pages_by_day'] + assert_not_nil pages_by_day + pages_by_day_size = pages_by_day.keys.inject(0) { |sum, day| sum + pages_by_day[day].size } + assert_equal all_pages.size, pages_by_day_size + all_pages.each do |page| + day = Date.new(page.revised_at.year, page.revised_at.month, page.revised_at.day) + assert pages_by_day[day].include?(page) + end + + assert_equal 'the web', r.template_objects['set_name'] + end + + def test_recently_revised_with_categorized_page + page2 = @wiki.write_page('wiki1', 'Page2', + "Page2 contents.\n" + + "category: categorized", + Time.now, Author.new('AnotherAuthor', '127.0.0.2'), test_renderer) + + r = process('recently_revised', 'web' => 'wiki1') + assert_success + + assert_equal %w(animals categorized trees), r.template_objects['categories'] + # no category is specified in params + assert_nil r.template_objects['category'] + assert_equal [@elephant, pages(:first_page), @home, pages(:my_way), pages(:no_wiki_word), @oak, page2, pages(:smart_engine), pages(:that_way)], r.template_objects['pages_in_category'], + "Pages are not as expected: " + + r.template_objects['pages_in_category'].map {|p| p.name}.inspect + assert_equal 'the web', r.template_objects['set_name'] + end + + def test_recently_revised_with_categorized_page_multiple_categories + r = process('recently_revised', 'web' => 'wiki1') + assert_success + + assert_equal ['animals', 'trees'], r.template_objects['categories'] + # no category is specified in params + assert_nil r.template_objects['category'] + assert_equal [@elephant, pages(:first_page), @home, pages(:my_way), pages(:no_wiki_word), @oak, pages(:smart_engine), pages(:that_way)], r.template_objects['pages_in_category'], + "Pages are not as expected: " + + r.template_objects['pages_in_category'].map {|p| p.name}.inspect + assert_equal 'the web', r.template_objects['set_name'] + end + + def test_recently_revised_with_specified_category + r = process('recently_revised', 'web' => 'wiki1', 'category' => 'animals') + assert_success + + assert_equal ['animals', 'trees'], r.template_objects['categories'] + # no category is specified in params + assert_equal 'animals', r.template_objects['category'] + assert_equal [@elephant], r.template_objects['pages_in_category'] + assert_equal "category 'animals'", r.template_objects['set_name'] + end + + + def test_revision + r = process 'revision', 'web' => 'wiki1', 'id' => 'HomePage', 'rev' => '1' + + assert_success + assert_equal @home, r.template_objects['page'] + assert_equal @home.revisions[0], r.template_objects['revision'] + end + + + def test_rollback + # rollback shows a form where a revision can be edited. + # its assigns the same as or revision + r = process 'rollback', 'web' => 'wiki1', 'id' => 'HomePage', 'rev' => '1' + + assert_success + assert_equal @home, r.template_objects['page'] + assert_equal @home.revisions[0], r.template_objects['revision'] + end + + def test_rss_with_content + r = process 'rss_with_content', 'web' => 'wiki1' + + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal [@elephant, @oak, pages(:no_wiki_word), pages(:that_way), pages(:smart_engine), pages(:my_way), pages(:first_page), @home], pages, + "Pages are not as expected: #{pages.map {|p| p.name}.inspect}" + assert !r.template_objects['hide_description'] + end + + def test_rss_with_content_when_blocked + @web.update_attributes(:password => 'aaa', :published => false) + @web = Web.find(@web.id) + + r = process 'rss_with_content', 'web' => 'wiki1' + + assert_equal 403, r.response_code + end + + + def test_rss_with_headlines + @title_with_spaces = @wiki.write_page('wiki1', 'Title With Spaces', + 'About spaces', 1.hour.ago, Author.new('TreeHugger', '127.0.0.2'), test_renderer) + + @request.host = 'localhost' + @request.port = 8080 + + r = process 'rss_with_headlines', 'web' => 'wiki1' + + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal [@elephant, @title_with_spaces, @oak, pages(:no_wiki_word), pages(:that_way), pages(:smart_engine), pages(:my_way), pages(:first_page), @home], pages, "Pages are not as expected: #{pages.map {|p| p.name}.inspect}" + assert r.template_objects['hide_description'] + + xml = REXML::Document.new(r.body) + + expected_page_links = + ['http://localhost:8080/wiki1/show/Elephant', + 'http://localhost:8080/wiki1/show/Title+With+Spaces', + 'http://localhost:8080/wiki1/show/Oak', + 'http://localhost:8080/wiki1/show/NoWikiWord', + 'http://localhost:8080/wiki1/show/ThatWay', + 'http://localhost:8080/wiki1/show/SmartEngine', + 'http://localhost:8080/wiki1/show/MyWay', + 'http://localhost:8080/wiki1/show/FirstPage', + 'http://localhost:8080/wiki1/show/HomePage', + ] + + assert_template_xpath_match '/rss/channel/link', + 'http://localhost:8080/wiki1/show/HomePage' + assert_template_xpath_match '/rss/channel/item/guid', expected_page_links + assert_template_xpath_match '/rss/channel/item/link', expected_page_links + end + + def test_rss_switch_links_to_published + @web.update_attributes(:password => 'aaa', :published => true) + @web = Web.find(@web.id) + + @request.host = 'foo.bar.info' + @request.port = 80 + + r = process 'rss_with_headlines', 'web' => 'wiki1' + + assert_success + xml = REXML::Document.new(r.body) + + expected_page_links = + ['http://foo.bar.info/wiki1/published/Elephant', + 'http://foo.bar.info/wiki1/published/Oak', + 'http://foo.bar.info/wiki1/published/NoWikiWord', + 'http://foo.bar.info/wiki1/published/ThatWay', + 'http://foo.bar.info/wiki1/published/SmartEngine', + 'http://foo.bar.info/wiki1/published/MyWay', + 'http://foo.bar.info/wiki1/published/FirstPage', + 'http://foo.bar.info/wiki1/published/HomePage'] + + assert_template_xpath_match '/rss/channel/link', + 'http://foo.bar.info/wiki1/published/HomePage' + assert_template_xpath_match '/rss/channel/item/guid', expected_page_links + assert_template_xpath_match '/rss/channel/item/link', expected_page_links + end + + def test_rss_with_params + setup_wiki_with_30_pages + + r = process 'rss_with_headlines', 'web' => 'wiki1' + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal 15, pages.size, 15 + + r = process 'rss_with_headlines', 'web' => 'wiki1', 'limit' => '5' + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal 5, pages.size + + r = process 'rss_with_headlines', 'web' => 'wiki1', 'limit' => '25' + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal 25, pages.size + + r = process 'rss_with_headlines', 'web' => 'wiki1', 'limit' => 'all' + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal 38, pages.size + + r = process 'rss_with_headlines', 'web' => 'wiki1', 'start' => '1976-10-16' + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal 23, pages.size + + r = process 'rss_with_headlines', 'web' => 'wiki1', 'end' => '1976-10-16' + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal 15, pages.size + + r = process 'rss_with_headlines', 'web' => 'wiki1', 'start' => '1976-10-01', 'end' => '1976-10-06' + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal 5, pages.size + end + + def test_rss_title_with_ampersand + # was ticket:143 + @wiki.write_page('wiki1', 'Title&With&Ampersands', + 'About spaces', 1.hour.ago, Author.new('NitPicker', '127.0.0.3'), test_renderer) + + r = process 'rss_with_headlines', 'web' => 'wiki1' + + assert r.body.include?('Home Page') + assert r.body.include?('Title&With&Ampersands') + end + + def test_rss_timestamp + new_page = @wiki.write_page('wiki1', 'PageCreatedAtTheBeginningOfCtime', + 'Created on 1 Jan 1970 at 0:00:00 Z', Time.at(0), Author.new('NitPicker', '127.0.0.3'), + test_renderer) + + r = process 'rss_with_headlines', 'web' => 'wiki1' + assert_template_xpath_match '/rss/channel/item/pubDate[9]', "Thu, 01 Jan 1970 00:00:00 Z" + end + + def test_save + r = process 'save', 'web' => 'wiki1', 'id' => 'NewPage', 'content' => 'Contents of a new page', + 'author' => 'AuthorOfNewPage' + + assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'NewPage' + assert_equal ['AuthorOfNewPage'], r.cookies['author'].value + assert_equal Time.utc(2030), r.cookies['author'].expires + new_page = @wiki.read_page('wiki1', 'NewPage') + assert_equal 'Contents of a new page', new_page.content + assert_equal 'AuthorOfNewPage', new_page.author + end + + def test_save_new_revision_of_existing_page + @home.lock(Time.now, 'Batman') + current_revisions = @home.revisions.size + + r = process 'save', 'web' => 'wiki1', 'id' => 'HomePage', 'content' => 'Revised HomePage', + 'author' => 'Batman' + + assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'HomePage' + assert_equal ['Batman'], r.cookies['author'].value + home_page = @wiki.read_page('wiki1', 'HomePage') + assert_equal current_revisions+1, home_page.revisions.size + assert_equal 'Revised HomePage', home_page.content + assert_equal 'Batman', home_page.author + assert !home_page.locked?(Time.now) + end + + def test_save_new_revision_identical_to_last + revisions_before = @home.revisions.size + @home.lock(Time.now, 'AnAuthor') + + r = process 'save', {'web' => 'wiki1', 'id' => 'HomePage', + 'content' => @home.revisions.last.content.dup, + 'author' => 'SomeOtherAuthor'}, {:return_to => '/wiki1/show/HomePage'} + + assert_redirected_to :action => 'edit', :web => 'wiki1', :id => 'HomePage' + assert_flash_has :error + assert r.flash[:error].kind_of?(Instiki::ValidationError) + + revisions_after = @home.revisions.size + assert_equal revisions_before, revisions_after + @home = Page.find(@home.id) + assert !@home.locked?(Time.now), 'HomePage should be unlocked if an edit was unsuccessful' + end + + def test_save_blank_author + r = process 'save', 'web' => 'wiki1', 'id' => 'NewPage', 'content' => 'Contents of a new page', + 'author' => '' + new_page = @wiki.read_page('wiki1', 'NewPage') + assert_equal 'AnonymousCoward', new_page.author + + r = process 'save', 'web' => 'wiki1', 'id' => 'AnotherPage', 'content' => 'Contents of a new page', + 'author' => ' ' + + another_page = @wiki.read_page('wiki1', 'AnotherPage') + assert_equal 'AnonymousCoward', another_page.author + end + + + def test_search + r = process 'search', 'web' => 'wiki1', 'query' => '\s[A-Z]ak' + + assert_redirected_to :action => 'show', :id => 'Oak' + end + + def test_search_multiple_results + r = process 'search', 'web' => 'wiki1', 'query' => 'All about' + + assert_success + assert_equal 'All about', r.template_objects['query'] + assert_equal [@elephant, @oak], r.template_objects['results'] + assert_equal [], r.template_objects['title_results'] + end + + def test_search_by_content_and_title + r = process 'search', 'web' => 'wiki1', 'query' => '(Oak|Elephant)' + + assert_success + assert_equal '(Oak|Elephant)', r.template_objects['query'] + assert_equal [@elephant, @oak], r.template_objects['results'] + assert_equal [@elephant, @oak], r.template_objects['title_results'] + end + + def test_search_zero_results + r = process 'search', 'web' => 'wiki1', 'query' => 'non-existant text' + + assert_success + assert_equal [], r.template_objects['results'] + assert_equal [], r.template_objects['title_results'] + end + + def test_show_page + r = process('show', 'id' => 'Oak', 'web' => 'wiki1') + assert_success + assert_tag :content => /All about oak/ + end + + def test_show_page_with_multiple_revisions + @wiki.write_page('wiki1', 'HomePage', 'Second revision of the HomePage end', Time.now, + Author.new('AnotherAuthor', '127.0.0.2'), test_renderer) + + r = process('show', 'id' => 'HomePage', 'web' => 'wiki1') + + assert_success + assert_match /Second revision of the end/, r.body + end + + def test_show_page_nonexistant_page + process('show', 'id' => 'UnknownPage', 'web' => 'wiki1') + assert_redirected_to :web => 'wiki1', :action => 'new', :id => 'UnknownPage' + end + + def test_show_no_page + r = process('show', 'id' => '', 'web' => 'wiki1') + assert_response :missing + + r = process('show', 'web' => 'wiki1') + assert_response :missing + end + + def test_tex + r = process('tex', 'web' => 'wiki1', 'id' => 'HomePage') + assert_success + + assert_equal "\\documentclass[12pt,titlepage]{article}\n\n\\usepackage[danish]{babel} " + + "%danske tekster\n\\usepackage[OT1]{fontenc} %rigtige danske bogstaver...\n" + + "\\usepackage{a4}\n\\usepackage{graphicx}\n\\usepackage{ucs}\n\\usepackage[utf8x]" + + "{inputenc}\n\\input epsf \n\n%----------------------------------------------------" + + "---------------\n\n\\begin{document}\n\n\\sloppy\n\n%-----------------------------" + + "--------------------------------------\n\n\\section*{HomePage}\n\nHisWay would be " + + "MyWay in kinda ThatWay in HisWay though MyWay \\OverThere -- see SmartEngine in that " + + "SmartEngineGUI\n\n\\end{document}", r.body + end + + + def test_web_list + another_wiki = @wiki.create_web('Another Wiki', 'another_wiki') + + r = process('web_list') + + assert_success + assert_equal [another_wiki, webs(:instiki), @web], r.template_objects['webs'] + end + +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 00000000..3afd1ae6 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,168 @@ +ENV['RAILS_ENV'] = 'test' + +# Expand the path to environment so that Ruby does not load it multiple times +# File.expand_path can be removed if Ruby 1.9 is in use. +require File.expand_path(File.dirname(__FILE__) + '/../config/environment') +require 'application' + +require 'test/unit' +require 'active_record/fixtures' +require 'action_controller/test_process' +require 'action_web_service/test_invoke' +require 'breakpoint' +require 'wiki_content' +require 'url_generator' + +Test::Unit::TestCase.pre_loaded_fixtures = false +Test::Unit::TestCase.use_transactional_fixtures = true +Test::Unit::TestCase.use_instantiated_fixtures = false +Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/" + +# activate PageObserver +PageObserver.instance + +class Test::Unit::TestCase + def create_fixtures(*table_names) + Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures", table_names) + end + + # Add more helper methods to be used by all tests here... + def set_web_property(property, value) + @web.update_attribute(property, value) + @page = Page.find(@page.id) + @wiki.webs[@web.name] = @web + end + + def setup_wiki_with_30_pages + ActiveRecord::Base.silence do + (1..30).each do |i| + @wiki.write_page('wiki1', "page#{i}", "Test page #{i}\ncategory: test", + Time.local(1976, 10, i, 12, 00, 00), Author.new('Dema', '127.0.0.2'), + test_renderer) + end + end + @web = Web.find(@web.id) + end + + def test_renderer(revision = nil) + PageRenderer.setup_url_generator(StubUrlGenerator.new) + PageRenderer.new(revision) + end + + def use_blank_wiki + Revision.destroy_all + Page.destroy_all + Web.destroy_all + end +end + +# This module is to be included in unit tests that involve matching chunks. +# It provides a easy way to test whether a chunk matches a particular string +# and any the values of any fields that should be set after a match. +class ContentStub < String + include ChunkManager + def initialize(str) + super + init_chunk_manager + end + def page_link(*); end +end + +module ChunkMatch + + # Asserts a number of tests for the given type and text. + def match(chunk_type, test_text, expected_chunk_state) + if chunk_type.respond_to? :pattern + assert_match(chunk_type.pattern, test_text) + end + + content = ContentStub.new(test_text) + chunk_type.apply_to(content) + + # Test if requested parts are correct. + expected_chunk_state.each_pair do |a_method, expected_value| + assert content.chunks.last.kind_of?(chunk_type) + assert_respond_to(content.chunks.last, a_method) + assert_equal(expected_value, content.chunks.last.send(a_method.to_sym), + "Wrong #{a_method} value") + end + end + + # Asserts that test_text doesn't match the chunk_type + def no_match(chunk_type, test_text) + if chunk_type.respond_to? :pattern + assert_no_match(chunk_type.pattern, test_text) + end + end +end + +class StubUrlGenerator < AbstractUrlGenerator + + def initialize + super(:doesnt_need_controller) + end + + def file_link(mode, name, text, web_name, known_file) + link = CGI.escape(name) + case mode + when :export + if known_file then %{#{text}} + else %{#{text}} end + when :publish + if known_file then %{#{text}} + else %{#{text}} end + else + if known_file + %{#{text}} + else + %{#{text}?} + end + end + end + + def page_link(mode, name, text, web_address, known_page) + link = CGI.escape(name) + case mode.to_sym + when :export + if known_page then %{#{text}} + else %{#{text}} end + when :publish + if known_page then %{#{text}} + else %{#{text}} end + else + if known_page + %{#{text}} + else + %{#{text}?} + end + end + end + + def pic_link(mode, name, text, web_name, known_pic) + link = CGI.escape(name) + case mode.to_sym + when :export + if known_pic then %{#{text}} + else %{#{text}} end + when :publish + if known_pic then %{#{text}} + else %{#{text}} end + else + if known_pic then %{#{text}} + else %{#{text}?} end + end + end +end + +module Test + module Unit + module Assertions + def assert_success(bypass_body_parsing = false) + assert_response :success + unless bypass_body_parsing + assert_nothing_raised(@response.body) { REXML::Document.new(@response.body) } + end + end + end + end +end diff --git a/test/unit/chunks/category_test.rb b/test/unit/chunks/category_test.rb new file mode 100755 index 00000000..6bc7627f --- /dev/null +++ b/test/unit/chunks/category_test.rb @@ -0,0 +1,22 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../test_helper' +require 'chunks/category' + +class CategoryTest < Test::Unit::TestCase + include ChunkMatch + + def test_single_category + match(Category, 'category: test', :list => ['test'], :hidden => nil) + match(Category, 'category : chunk test ', :list => ['chunk test'], :hidden => nil) + match(Category, ':category: test', :list => ['test'], :hidden => ':') + end + + def test_multiple_categories + match(Category, 'category: test, multiple', :list => ['test', 'multiple'], :hidden => nil) + match(Category, 'category : chunk test , multi category,regression test case ', + :list => ['chunk test','multi category','regression test case'], :hidden => nil + ) + end + +end diff --git a/test/unit/chunks/nowiki_test.rb b/test/unit/chunks/nowiki_test.rb new file mode 100755 index 00000000..8af5a645 --- /dev/null +++ b/test/unit/chunks/nowiki_test.rb @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../test_helper' +require 'chunks/nowiki' + +class NoWikiTest < Test::Unit::TestCase + include ChunkMatch + + def test_simple_nowiki + match(NoWiki, 'This sentence contains [[raw text]]. Do not touch!', + :plain_text => '[[raw text]]' + ) + end + +end diff --git a/test/unit/chunks/wiki_test.rb b/test/unit/chunks/wiki_test.rb new file mode 100755 index 00000000..82c2546c --- /dev/null +++ b/test/unit/chunks/wiki_test.rb @@ -0,0 +1,98 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../test_helper' +require 'chunks/wiki' + +class WikiTest < Test::Unit::TestCase + + include ChunkMatch + + def test_simple + match(WikiChunk::Word, 'This is a WikiWord okay?', :page_name => 'WikiWord') + end + + def test_escaped + # escape is only implemented in WikiChunk::Word + match(WikiChunk::Word, 'Do not link to an \EscapedWord', + :page_name => 'EscapedWord', :escaped_text => 'EscapedWord' + ) + end + + def test_simple_brackets + match(WikiChunk::Link, 'This is a [[bracketted link]]', :page_name => 'bracketted link') + end + + def test_void_brackets + # double brackets woith only spaces inside are not a WikiLink + no_match(WikiChunk::Link, "This [[ ]] are [[]] no [[ \t ]] links") + end + + def test_brackets_strip_spaces + match(WikiChunk::Link, + "This is a [[Sperberg-McQueen \t ]] link with trailing spaces to strip", + :page_name => 'Sperberg-McQueen') + match(WikiChunk::Link, + "This is a [[ \t Sperberg-McQueen]] link with leading spaces to strip", + :page_name => 'Sperberg-McQueen') + match(WikiChunk::Link, + 'This is a [[ Sperberg-McQueen ]] link with spaces around it to strip', + :page_name => 'Sperberg-McQueen') + match(WikiChunk::Link, + 'This is a [[ Sperberg McQueen ]] link with spaces inside and around it', + :page_name => 'Sperberg McQueen') + end + + def test_complex_brackets + match(WikiChunk::Link, 'This is a tricky link [[Sperberg-McQueen]]', + :page_name => 'Sperberg-McQueen') + end + + def test_include_chunk_pattern + content = 'This is a [[!include pagename]] and [[!include WikiWord]] but [[blah]]' + recognized_includes = content.scan(Include.pattern).collect { |m| m[0] } + assert_equal %w(pagename WikiWord), recognized_includes + end + + def test_textile_link + textile_link = ContentStub.new('"Here is a special link":SpecialLink') + WikiChunk::Word.apply_to(textile_link) + assert_equal '"Here is a special link":SpecialLink', textile_link + assert textile_link.chunks.empty? + end + + def test_file_types + # only link + assert_link_parsed_as 'only text', 'only text', :show, '[[only text]]' + # link and text + assert_link_parsed_as 'page name', 'link text', :show, '[[page name|link text]]' + # link and type (file) + assert_link_parsed_as 'foo.tar.gz', 'foo.tar.gz', :file, '[[foo.tar.gz:file]]' + # link and type (pic) + assert_link_parsed_as 'foo.tar.gz', 'foo.tar.gz', :pic, '[[foo.tar.gz:pic]]' + # link, text and type + assert_link_parsed_as 'foo.tar.gz', 'FooTar', :file, '[[foo.tar.gz|FooTar:file]]' + + # NEGATIVE TEST CASES + + # empty page name + assert_link_parsed_as '|link text?', '|link text?', :file, '[[|link text?:file]]' + # empty link text + assert_link_parsed_as 'page name?|', 'page name?|', :file, '[[page name?|:file]]' + # empty link type + assert_link_parsed_as 'page name', 'link?:', :show, '[[page name|link?:]]' + # unknown link type + assert_link_parsed_as 'page name:create_system', 'page name:create_system', :show, + '[[page name:create_system]]' + end + + def assert_link_parsed_as(expected_page_name, expected_link_text, expected_link_type, link) + link_to_file = ContentStub.new(link) + WikiChunk::Link.apply_to(link_to_file) + chunk = link_to_file.chunks.last + assert chunk + assert_equal expected_page_name, chunk.page_name + assert_equal expected_link_text, chunk.link_text + assert_equal expected_link_type, chunk.link_type + end + +end diff --git a/test/unit/diff_test.rb b/test/unit/diff_test.rb new file mode 100755 index 00000000..67a8a71b --- /dev/null +++ b/test/unit/diff_test.rb @@ -0,0 +1,110 @@ +#!/usr/bin/env ruby + +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') +require 'diff' + +class DiffTest < Test::Unit::TestCase + + include HTMLDiff + + def setup + @builder = DiffBuilder.new('old', 'new') + end + + def test_start_of_tag + assert @builder.start_of_tag?('<') + assert(!@builder.start_of_tag?('>')) + assert(!@builder.start_of_tag?('a')) + end + + def test_end_of_tag + assert @builder.end_of_tag?('>') + assert(!@builder.end_of_tag?('<')) + assert(!@builder.end_of_tag?('a')) + end + + def test_whitespace + assert @builder.whitespace?(" ") + assert @builder.whitespace?("\n") + assert @builder.whitespace?("\r") + assert(!@builder.whitespace?("a")) + end + + def test_convert_html_to_list_of_words_simple + assert_equal( + ['the', ' ', 'original', ' ', 'text'], + @builder.convert_html_to_list_of_words('the original text')) + end + + def test_convert_html_to_list_of_words_should_separate_endlines + assert_equal( + ['a', "\n", 'b', "\r", 'c'], + @builder.convert_html_to_list_of_words("a\nb\rc")) + end + + def test_convert_html_to_list_of_words_should_not_compress_whitespace + assert_equal( + ['a', ' ', 'b', ' ', 'c', "\r \n ", 'd'], + @builder.convert_html_to_list_of_words("a b c\r \n d")) + end + + def test_convert_html_to_list_of_words_should_handle_tags_well + assert_equal( + ['

    ', 'foo', ' ', 'bar', '

    '], + @builder.convert_html_to_list_of_words("

    foo bar

    ")) + end + + def test_convert_html_to_list_of_words_interesting + assert_equal( + ['

    ', 'this', ' ', 'is', '

    ', "\r\n", '

    ', 'the', ' ', 'new', ' ', 'string', + '

    ', "\r\n", '

    ', 'around', ' ', 'the', ' ', 'world', '

    '], + @builder.convert_html_to_list_of_words( + "

    this is

    \r\n

    the new string

    \r\n

    around the world

    ")) + end + + def test_html_diff_simple + a = 'this was the original string' + b = 'this is the new string' + assert_equal('this wasis the ' + + 'originalnew string', + diff(a, b)) + end + + def test_html_diff_with_multiple_paragraphs + a = "

    this was the original string

    " + b = "

    this is

    \r\n

    the new string

    \r\n

    around the world

    " + + # Some of this expected result is accidental to implementation. + # At least it's well-formed and more or less correct. + assert_equal( + "

    this wasis

    "+ + "\r\n

    the " + + "originalnew" + + " string

    \r\n" + + "

    around the world

    ", + diff(a, b)) + end + + # FIXME this test fails (ticket #67, http://dev.instiki.org/ticket/67) + def test_html_diff_preserves_endlines_in_pre + a = "
    \na\nb\nc\n
    " + b = "
    \n
    " + assert_equal( + "
    \na\nb\nc\n
    ", + diff(a, b)) + end + + def test_html_diff_with_tags + a = "" + b = "
    foo
    " + assert_equal '
    foo
    ', diff(a, b) + end + + def test_diff_for_tag_change + a = "x" + b = "x" + # FIXME sad, but true - this case produces an invalid XML. If handle this you can, strong your foo is. + assert_equal 'x', diff(a, b) + end + +end diff --git a/test/unit/page_renderer_test.rb b/test/unit/page_renderer_test.rb new file mode 100644 index 00000000..54f5990e --- /dev/null +++ b/test/unit/page_renderer_test.rb @@ -0,0 +1,389 @@ +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') + +class PageRendererTest < Test::Unit::TestCase + fixtures :webs, :pages, :revisions, :system, :wiki_references + + def setup + @wiki = Wiki.new + @web = webs(:test_wiki) + @page = pages(:home_page) + @revision = revisions(:home_page_second_revision) + end + + def test_wiki_word_linking + @web.add_page('SecondPage', 'Yo, yo. Have you EverBeenHated', + Time.now, 'DavidHeinemeierHansson', test_renderer) + + assert_equal('

    Yo, yo. Have you Ever Been Hated' + + '?

    ', + rendered_content(@web.page("SecondPage"))) + + @web.add_page('EverBeenHated', 'Yo, yo. Have you EverBeenHated', Time.now, + 'DavidHeinemeierHansson', test_renderer) + assert_equal('

    Yo, yo. Have you Ever Been Hated

    ', + rendered_content(@web.page("SecondPage"))) + end + + def test_wiki_words + assert_equal %w( HisWay MyWay SmartEngine SmartEngineGUI ThatWay ), + test_renderer(@revision).wiki_words.sort + + @wiki.write_page('wiki1', 'NoWikiWord', 'hey you!', Time.now, 'Me', test_renderer) + assert_equal [], test_renderer(@wiki.read_page('wiki1', 'NoWikiWord').revisions.last).wiki_words + end + + def test_existing_pages + assert_equal %w( MyWay SmartEngine ThatWay ), test_renderer(@revision).existing_pages.sort + end + + def test_unexisting_pages + assert_equal %w( HisWay SmartEngineGUI ), test_renderer(@revision).unexisting_pages.sort + end + + def test_content_with_wiki_links + assert_equal '

    His Way? ' + + 'would be My Way in kinda ' + + 'That Way in ' + + 'His Way? ' + + 'though My Way OverThere—see ' + + 'Smart Engine in that ' + + 'Smart Engine GUI' + + '?

    ', + test_renderer(@revision).display_content + end + + def test_markdown + set_web_property :markup, :markdown + + assert_markup_parsed_as( + %{

    My Headline

    \n\n

    that } + + %{Smart Engine GUI?

    }, + "My Headline\n===========\n\nthat SmartEngineGUI") + + code_block = [ + 'This is a code block:', + '', + ' def a_method(arg)', + ' return ThatWay', + '', + 'Nice!' + ].join("\n") + + assert_markup_parsed_as( + %{

    This is a code block:

    \n\n
    def a_method(arg)\n} +
    +        %{return ThatWay\n
    \n\n

    Nice!

    }, + code_block) + end + + def test_markdown_hyperlink_with_slash + # in response to a bug, see http://dev.instiki.org/attachment/ticket/177 + set_web_property :markup, :markdown + + assert_markup_parsed_as( + '

    text

    ', + '[text](http://example/with/slash)') + end + + def test_mixed_formatting + textile_and_markdown = [ + 'Markdown heading', + '================', + '', + 'h2. Textile heading', + '', + '*some* **text** _with_ -styles-', + '', + '* list 1', + '* list 2' + ].join("\n") + + set_web_property :markup, :markdown + assert_markup_parsed_as( + "

    Markdown heading

    \n\n" + + "

    h2. Textile heading

    \n\n" + + "

    some text with -styles-

    \n\n" + + "
      \n
    • list 1
    • \n
    • list 2
    • \n
    ", + textile_and_markdown) + + set_web_property :markup, :textile + assert_markup_parsed_as( + "

    Markdown heading
    ================

    \n\n\n\t

    Textile heading

    " + + "\n\n\n\t

    some text with styles

    " + + "\n\n\n\t
      \n\t
    • list 1
    • \n\t\t
    • list 2
    • \n\t
    ", + textile_and_markdown) + + set_web_property :markup, :mixed + assert_markup_parsed_as( + "

    Markdown heading

    \n\n\n\t

    Textile heading

    \n\n\n\t" + + "

    some text with styles

    \n\n\n\t" + + "
      \n\t
    • list 1
    • \n\t\t
    • list 2
    • \n\t
    ", + textile_and_markdown) + end + + def test_rdoc + set_web_property :markup, :rdoc + + @revision = Revision.new(:page => @page, :content => '+hello+ that SmartEngineGUI', + :author => Author.new('DavidHeinemeierHansson')) + + assert_equal "hello that Smart Engine GUI" + + "?\n\n", + test_renderer(@revision).display_content + end + + def test_content_with_auto_links + assert_markup_parsed_as( + '

    http://www.loudthinking.com/ ' + + 'points to That Way from ' + + 'david@loudthinking.com

    ', + 'http://www.loudthinking.com/ points to ThatWay from david@loudthinking.com') + + end + + def test_content_with_aliased_links + assert_markup_parsed_as( + '

    Would a clever motor' + + ' go by any other name?

    ', + 'Would a [[SmartEngine|clever motor]] go by any other name?') + end + + def test_content_with_wikiword_in_em + assert_markup_parsed_as( + '

    should we go ' + + 'That Way or This Way?' + + '

    ', + '_should we go ThatWay or ThisWay _') + end + + def test_content_with_wikiword_in_tag + assert_markup_parsed_as( + '

    That is some Stylish Emphasis

    ', + 'That is some Stylish Emphasis') + end + + def test_content_with_escaped_wikiword + # there should be no wiki link + assert_markup_parsed_as('

    WikiWord

    ', '\WikiWord') + end + + def test_content_with_pre_blocks + assert_markup_parsed_as( + '

    A class SmartEngine end would not mark up

    CodeBlocks

    ', + 'A class SmartEngine end would not mark up
    CodeBlocks
    ') + end + + def test_content_with_autolink_in_parentheses + assert_markup_parsed_as( + '

    The W3C body (' + + 'http://www.w3c.org) sets web standards

    ', + 'The W3C body (http://www.w3c.org) sets web standards') + end + + def test_content_with_link_in_parentheses + assert_markup_parsed_as( + '

    (What is a wiki?)

    ', + '("What is a wiki?":http://wiki.org/wiki.cgi?WhatIsWiki)') + end + + def test_content_with_image_link + assert_markup_parsed_as( + '

    This is a Textile image link.

    ', + 'This !http://hobix.com/sample.jpg! is a Textile image link.') + end + + def test_content_with_inlined_img_tag + assert_markup_parsed_as( + '

    This is an inline image link.

    ', + 'This is an inline image link.') + + assert_markup_parsed_as( + '

    This is an inline image link.

    ', + 'This is an inline image link.') + end + + def test_nowiki_tag + assert_markup_parsed_as( + '

    Do not mark up [[this text]] or http://www.thislink.com.

    ', + 'Do not mark up [[this text]] ' + + 'or http://www.thislink.com.') + end + + def test_multiline_nowiki_tag + assert_markup_parsed_as( + "

    Do not mark \n up [[this text]] \nand http://this.url.com but markup " + + 'this?

    ', + "Do not mark \n up [[this text]] \n" + + "and http://this.url.com but markup [[this]]") + end + + def test_content_with_bracketted_wiki_word + set_web_property :brackets_only, true + assert_markup_parsed_as( + '

    This is a WikiWord and a tricky name ' + + 'Sperberg-McQueen?.

    ', + 'This is a WikiWord and a tricky name [[Sperberg-McQueen]].') + end + + def test_content_for_export + assert_equal '

    His Way would be ' + + 'My Way in kinda ' + + 'That Way in ' + + 'His Way though ' + + 'My Way OverThere—see ' + + 'Smart Engine in that ' + + 'Smart Engine GUI

    ', + test_renderer(@revision).display_content_for_export + end + + def test_double_replacing + @revision.content = "VersionHistory\r\n\r\ncry VersionHistory" + assert_equal '

    Version History' + + "?

    \n\n\n\t

    cry " + + 'Version History?' + + '

    ', + test_renderer(@revision).display_content + + @revision.content = "f\r\nVersionHistory\r\n\r\ncry VersionHistory" + assert_equal "

    f
    Version History" + + "?

    \n\n\n\t

    cry " + + "Version History?" + + "

    ", + test_renderer(@revision).display_content + end + + def test_difficult_wiki_words + @revision.content = "[[It's just awesome GUI!]]" + assert_equal "

    It's just awesome GUI!" + + "?

    ", + test_renderer(@revision).display_content + end + + def test_revisions_diff + Revision.create(:page => @page, :content => 'What a blue and lovely morning', + :author => Author.new('DavidHeinemeierHansson'), :revised_at => Time.now) + Revision.create(:page => @page, :content => 'What a red and lovely morning today', + :author => Author.new('DavidHeinemeierHansson'), :revised_at => Time.now) + + assert_equal "

    What a bluered" + + " and lovely morning today

    ", test_renderer(@page.revisions.last).display_diff + end + + def test_link_to_file + assert_markup_parsed_as( + '

    doc.pdf?

    ', + '[[doc.pdf:file]]') + end + + def test_link_to_pic + WikiFile.delete_all + require 'fileutils' + FileUtils.rm_rf("#{RAILS_ROOT}/public/wiki1/files/*") + @web.wiki_files.create(:file_name => 'square.jpg', :description => 'Square', :content => 'never mind') + assert_markup_parsed_as( + '

    Square

    ', + '[[square.jpg|Square:pic]]') + assert_markup_parsed_as( + '

    square.jpg

    ', + '[[square.jpg:pic]]') + end + + def test_link_to_non_existant_pic + assert_markup_parsed_as( + '

    NonExistant?' + + '

    ', + '[[NonExistant.jpg|NonExistant:pic]]') + assert_markup_parsed_as( + '

    NonExistant.jpg?' + + '

    ', + '[[NonExistant.jpg:pic]]') + end + + def test_wiki_link_with_colon + assert_markup_parsed_as( + '

    With:Colon?

    ', + '[[With:Colon]]') + end + + def test_list_with_tildas + list_with_tildas = <<-EOL + * "a":~b + * c~ d + EOL + + assert_markup_parsed_as( + "
      \n\t
    • a
    • \n\t\t
    • c~ d
    • \n\t
    ", + list_with_tildas) + end + + def test_textile_image_in_mixed_wiki + set_web_property :markup, :mixed + assert_markup_parsed_as( + "

    \"\"\nss

    ", + "!http://google.com!\r\nss") + end + + + def test_references_creation_links + new_page = @web.add_page('NewPage', 'HomePage NewPage', + Time.local(2004, 4, 4, 16, 50), 'AlexeyVerkhovsky', test_renderer) + + references = new_page.wiki_references(true) + assert_equal 2, references.size + assert_equal 'HomePage', references[0].referenced_name + assert_equal WikiReference::LINKED_PAGE, references[0].link_type + assert_equal 'NewPage', references[1].referenced_name + assert_equal WikiReference::LINKED_PAGE, references[1].link_type + end + + def test_references_creation_includes + new_page = @web.add_page('NewPage', '[[!include IncludedPage]]', + Time.local(2004, 4, 4, 16, 50), 'AlexeyVerkhovsky', test_renderer) + + references = new_page.wiki_references(true) + assert_equal 1, references.size + assert_equal 'IncludedPage', references[0].referenced_name + assert_equal WikiReference::INCLUDED_PAGE, references[0].link_type + end + + def test_references_creation_categories + new_page = @web.add_page('NewPage', "Foo\ncategory: NewPageCategory", + Time.local(2004, 4, 4, 16, 50), 'AlexeyVerkhovsky', test_renderer) + + references = new_page.wiki_references(true) + assert_equal 1, references.size + assert_equal 'NewPageCategory', references[0].referenced_name + assert_equal WikiReference::CATEGORY, references[0].link_type + end + + def test_rendering_included_page_under_different_modes + included = @web.add_page('Included', 'link to HomePage', Time.now, 'AnAuthor', test_renderer) + main = @web.add_page('Main', '[[!include Included]]', Time.now, 'AnAuthor', test_renderer) + + assert_equal '

    link to Home Page

    ', + test_renderer(main).display_content + assert_equal '

    link to Home Page

    ', + test_renderer(main).display_published + assert_equal '

    link to Home Page

    ', + test_renderer(main).display_content_for_export + end + + private + + def add_sample_pages + @in_love = @web.add_page('EverBeenInLove', 'Who am I me', + Time.local(2004, 4, 4, 16, 50), 'DavidHeinemeierHansson', test_renderer) + @hated = @web.add_page('EverBeenHated', 'I am me EverBeenHated', + Time.local(2004, 4, 4, 16, 51), 'DavidHeinemeierHansson', test_renderer) + end + + def assert_markup_parsed_as(expected_output, input) + revision = Revision.new(:page => @page, :content => input, :author => Author.new('AnAuthor')) + assert_equal expected_output, test_renderer(revision).display_content, 'Rendering output not as expected' + end + + def rendered_content(page) + test_renderer(page.revisions.last).display_content + end + +end \ No newline at end of file diff --git a/test/unit/page_test.rb b/test/unit/page_test.rb new file mode 100644 index 00000000..5513cd48 --- /dev/null +++ b/test/unit/page_test.rb @@ -0,0 +1,122 @@ +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') + +class PageTest < Test::Unit::TestCase + fixtures :webs, :pages, :revisions, :system + + def setup + @page = pages(:first_page) + end + + + def test_lock + assert !@page.locked?(Time.local(2004, 4, 4, 16, 50)) + + @page.lock(Time.local(2004, 4, 4, 16, 30), "DavidHeinemeierHansson") + + assert @page.locked?(Time.local(2004, 4, 4, 16, 50)) + assert !@page.locked?(Time.local(2004, 4, 4, 17, 1)) + + @page.unlock + + assert !@page.locked?(Time.local(2004, 4, 4, 16, 50)) + end + + def test_lock_duration + @page.lock(Time.local(2004, 4, 4, 16, 30), "DavidHeinemeierHansson") + + assert_equal 15, @page.lock_duration(Time.local(2004, 4, 4, 16, 45)) + end + + def test_plain_name + assert_equal "First Page", @page.plain_name + end + + def test_revise + @page.revise('HisWay would be MyWay in kinda lame', Time.local(2004, 4, 4, 16, 55), + 'MarianneSyhler', test_renderer) + @page.reload + + assert_equal 2, @page.revisions.length, 'Should have two revisions' + assert_equal 'MarianneSyhler', @page.current_revision.author.to_s, + 'Mary should be the author now' + assert_equal 'DavidHeinemeierHansson', @page.revisions.first.author.to_s, + 'David was the first author' + end + + def test_revise_continous_revision + @page.revise('HisWay would be MyWay in kinda lame', Time.local(2004, 4, 4, 16, 55), + 'MarianneSyhler', test_renderer) + @page.reload + assert_equal 2, @page.revisions.length + assert_equal 'HisWay would be MyWay in kinda lame', @page.content + + # consecutive revision by the same author within 30 minutes doesn't create a new revision + @page.revise('HisWay would be MyWay in kinda update', Time.local(2004, 4, 4, 16, 57), + 'MarianneSyhler', test_renderer) + @page.reload + assert_equal 2, @page.revisions.length + assert_equal 'HisWay would be MyWay in kinda update', @page.content + assert_equal Time.local(2004, 4, 4, 16, 57), @page.revised_at + + # but consecutive revision by another author results in a new revision + @page.revise('HisWay would be MyWay in the house', Time.local(2004, 4, 4, 16, 58), + 'DavidHeinemeierHansson', test_renderer) + @page.reload + assert_equal 3, @page.revisions.length + assert_equal 'HisWay would be MyWay in the house', @page.content + + # consecutive update after 30 minutes since the last one also creates a new revision, + # even when it is by the same author + @page.revise('HisWay would be MyWay in my way', Time.local(2004, 4, 4, 17, 30), + 'DavidHeinemeierHansson', test_renderer) + @page.reload + assert_equal 4, @page.revisions.length + end + + def test_revise_content_unchanged + last_revision_before = @page.current_revision + revisions_number_before = @page.revisions.size + + assert_raises(Instiki::ValidationError) { + @page.revise(@page.current_revision.content, Time.now, 'AlexeyVerkhovsky', test_renderer) + } + + assert_equal last_revision_before, @page.current_revision(true) + assert_equal revisions_number_before, @page.revisions.size + end + + def test_revise_changes_references_from_wanted_to_linked_for_new_pages + web = Web.find(1) + new_page = Page.new(:web => web, :name => 'NewPage') + new_page.revise('Reference to WantedPage, and to WantedPage2', Time.now, 'AlexeyVerkhovsky', + test_renderer) + + references = new_page.wiki_references(true) + assert_equal 2, references.size + assert_equal 'WantedPage', references[0].referenced_name + assert_equal WikiReference::WANTED_PAGE, references[0].link_type + assert_equal 'WantedPage2', references[1].referenced_name + assert_equal WikiReference::WANTED_PAGE, references[1].link_type + + wanted_page = Page.new(:web => web, :name => 'WantedPage') + wanted_page.revise('And here it is!', Time.now, 'AlexeyVerkhovsky', test_renderer) + + # link type stored for NewPage -> WantedPage reference should change from WANTED to LINKED + # reference NewPage -> WantedPage2 should remain the same + references = new_page.wiki_references(true) + assert_equal 2, references.size + assert_equal 'WantedPage', references[0].referenced_name + assert_equal WikiReference::LINKED_PAGE, references[0].link_type + assert_equal 'WantedPage2', references[1].referenced_name + assert_equal WikiReference::WANTED_PAGE, references[1].link_type + end + + def test_rollback + @page.revise("spot two", Time.now, "David", test_renderer) + @page.revise("spot three", Time.now + 2000, "David", test_renderer) + assert_equal 3, @page.revisions(true).length, "Should have three revisions" + @page.current_revision(true) + @page.rollback(0, Time.now, '127.0.0.1', test_renderer) + assert_equal "HisWay would be MyWay in kinda ThatWay in HisWay though MyWay \\\\OverThere -- see SmartEngine in that SmartEngineGUI", @page.current_revision(true).content + end +end diff --git a/test/unit/redcloth_for_tex_test.rb b/test/unit/redcloth_for_tex_test.rb new file mode 100755 index 00000000..3556beaf --- /dev/null +++ b/test/unit/redcloth_for_tex_test.rb @@ -0,0 +1,69 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../test_helper' +require 'redcloth_for_tex' + +class RedClothForTexTest < Test::Unit::TestCase + def test_basics + assert_equal '{\bf First Page}', RedClothForTex.new("*First Page*").to_tex + assert_equal '{\em First Page}', RedClothForTex.new("_First Page_").to_tex + assert_equal "\\begin{itemize}\n\t\\item A\n\t\t\\item B\n\t\t\\item C\n\t\\end{itemize}", RedClothForTex.new("* A\n* B\n* C").to_tex + end + + def test_blocks + assert_equal '\section*{hello}', RedClothForTex.new("h1. hello").to_tex + assert_equal '\subsection*{hello}', RedClothForTex.new("h2. hello").to_tex + end + + def test_table_of_contents + +source = < 'Abe', 'B' => 'Babe')) + end + + def test_entities + assert_equal "Beck \\& Fowler are 100\\% cool", RedClothForTex.new("Beck & Fowler are 100% cool").to_tex + end + + def test_bracket_links + assert_equal "such a Horrible Day, but I won't be Made Useless", RedClothForTex.new("such a [[Horrible Day]], but I won't be [[Made Useless]]").to_tex + end + + def test_footnotes_on_abbreviations + assert_equal( + "such a Horrible Day\\footnote{1}, but I won't be Made Useless", + RedClothForTex.new("such a [[Horrible Day]][1], but I won't be [[Made Useless]]").to_tex + ) + end + + def test_subsection_depth + assert_equal "\\subsubsection*{Hello}", RedClothForTex.new("h4. Hello").to_tex + end +end diff --git a/test/unit/uri_test.rb b/test/unit/uri_test.rb new file mode 100755 index 00000000..29326c3b --- /dev/null +++ b/test/unit/uri_test.rb @@ -0,0 +1,217 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../test_helper' +require 'chunks/uri' + +class URITest < Test::Unit::TestCase + include ChunkMatch + + def test_non_matches + assert_conversion_does_not_apply(URIChunk, 'There is no URI here') + assert_conversion_does_not_apply(URIChunk, + 'One gemstone is the garnet:reddish in colour, like ruby') + end + + def test_simple_uri + # Simplest case + match(URIChunk, 'http://www.example.com', + :scheme =>'http', :host =>'www.example.com', :path => nil, + :link_text => 'http://www.example.com' + ) + # With trailing slash + match(URIChunk, 'http://www.example.com/', + :scheme =>'http', :host =>'www.example.com', :path => '/', + :link_text => 'http://www.example.com/' + ) + # Without http:// + match(URIChunk, 'www.example.com', + :scheme =>'http', :host =>'www.example.com', :link_text => 'www.example.com' + ) + # two parts + match(URIChunk, 'example.com', + :scheme =>'http',:host =>'example.com', :link_text => 'example.com' + ) + # "unusual" base domain (was a bug in an early version) + match(URIChunk, 'http://example.com.au/', + :scheme =>'http', :host =>'example.com.au', :link_text => 'http://example.com.au/' + ) + # "unusual" base domain without http:// + match(URIChunk, 'example.com.au', + :scheme =>'http', :host =>'example.com.au', :link_text => 'example.com.au' + ) + # Another "unusual" base domain + match(URIChunk, 'http://www.example.co.uk/', + :scheme =>'http', :host =>'www.example.co.uk', + :link_text => 'http://www.example.co.uk/' + ) + match(URIChunk, 'example.co.uk', + :scheme =>'http', :host =>'example.co.uk', :link_text => 'example.co.uk' + ) + # With some path at the end + match(URIChunk, 'http://moinmoin.wikiwikiweb.de/HelpOnNavigation', + :scheme => 'http', :host => 'moinmoin.wikiwikiweb.de', :path => '/HelpOnNavigation', + :link_text => 'http://moinmoin.wikiwikiweb.de/HelpOnNavigation' + ) + # With some path at the end, and withot http:// prefix + match(URIChunk, 'moinmoin.wikiwikiweb.de/HelpOnNavigation', + :scheme => 'http', :host => 'moinmoin.wikiwikiweb.de', :path => '/HelpOnNavigation', + :link_text => 'moinmoin.wikiwikiweb.de/HelpOnNavigation' + ) + # With a port number + match(URIChunk, 'http://www.example.com:80', + :scheme =>'http', :host =>'www.example.com', :port => '80', :path => nil, + :link_text => 'http://www.example.com:80') + # With a port number and a path + match(URIChunk, 'http://www.example.com.tw:80/HelpOnNavigation', + :scheme =>'http', :host =>'www.example.com.tw', :port => '80', :path => '/HelpOnNavigation', + :link_text => 'http://www.example.com.tw:80/HelpOnNavigation') + # With a query + match(URIChunk, 'http://www.example.com.tw:80/HelpOnNavigation?arg=val', + :scheme =>'http', :host =>'www.example.com.tw', :port => '80', :path => '/HelpOnNavigation', + :query => 'arg=val', + :link_text => 'http://www.example.com.tw:80/HelpOnNavigation?arg=val') + # Query with two arguments + match(URIChunk, 'http://www.example.com.tw:80/HelpOnNavigation?arg=val&arg2=val2', + :scheme =>'http', :host =>'www.example.com.tw', :port => '80', :path => '/HelpOnNavigation', + :query => 'arg=val&arg2=val2', + :link_text => 'http://www.example.com.tw:80/HelpOnNavigation?arg=val&arg2=val2') + # HTTPS + match(URIChunk, 'https://www.example.com', + :scheme =>'https', :host =>'www.example.com', :port => nil, :path => nil, :query => nil, + :link_text => 'https://www.example.com') + # FTP + match(URIChunk, 'ftp://www.example.com', + :scheme =>'ftp', :host =>'www.example.com', :port => nil, :path => nil, :query => nil, + :link_text => 'ftp://www.example.com') + # mailto + match(URIChunk, 'mailto:jdoe123@example.com', + :scheme =>'mailto', :host =>'example.com', :port => nil, :path => nil, :query => nil, + :user => 'jdoe123', :link_text => 'mailto:jdoe123@example.com') + # something nonexistant + match(URIChunk, 'foobar://www.example.com', + :scheme =>'foobar', :host =>'www.example.com', :port => nil, :path => nil, :query => nil, + :link_text => 'foobar://www.example.com') + + # Soap opera (the most complex case imaginable... well, not really, there should be more evil) + match(URIChunk, 'http://www.example.com.tw:80/~jdoe123/Help%20Me%20?arg=val&arg2=val2', + :scheme =>'http', :host =>'www.example.com.tw', :port => '80', + :path => '/~jdoe123/Help%20Me%20', :query => 'arg=val&arg2=val2', + :link_text => 'http://www.example.com.tw:80/~jdoe123/Help%20Me%20?arg=val&arg2=val2') + + # from 0.9 bug reports + match(URIChunk, 'http://www2.pos.to/~tosh/ruby/rdtool/en/doc/rd-draft.html', + :scheme =>'http', :host => 'www2.pos.to', + :path => '/~tosh/ruby/rdtool/en/doc/rd-draft.html') + + match(URIChunk, 'http://support.microsoft.com/default.aspx?scid=kb;en-us;234562', + :scheme =>'http', :host => 'support.microsoft.com', :path => '/default.aspx', + :query => 'scid=kb;en-us;234562') + end + + def test_email_uri + match(URIChunk, 'mail@example.com', + :user => 'mail', :host => 'example.com', :link_text => 'mail@example.com' + ) + end + + def test_non_email + # The @ is part of the normal text, but 'example.com' is marked up. + match(URIChunk, 'Not an email: @example.com', :user => nil, :uri => 'http://example.com') + end + + def test_textile_image + assert_conversion_does_not_apply(URIChunk, + 'This !http://hobix.com/sample.jpg! is a Textile image link.') + end + + def test_textile_link + assert_conversion_does_not_apply(URIChunk, + 'This "hobix (hobix)":http://hobix.com/sample.jpg is a Textile link.') + # just to be sure ... + match(URIChunk, 'This http://hobix.com/sample.jpg should match', + :link_text => 'http://hobix.com/sample.jpg') + end + + def test_inline_html + assert_conversion_does_not_apply(URIChunk, '') + assert_conversion_does_not_apply(URIChunk, "") + end + + def test_non_uri + # "so" is a valid country code; "libproxy.so" is a valid url + match(URIChunk, 'libproxy.so', :link_text => 'libproxy.so') + + assert_conversion_does_not_apply URIChunk, 'httpd.conf' + assert_conversion_does_not_apply URIChunk, 'ld.so.conf' + assert_conversion_does_not_apply URIChunk, 'index.jpeg' + assert_conversion_does_not_apply URIChunk, 'index.jpg' + assert_conversion_does_not_apply URIChunk, 'file.txt' + assert_conversion_does_not_apply URIChunk, 'file.doc' + assert_conversion_does_not_apply URIChunk, 'file.pdf' + assert_conversion_does_not_apply URIChunk, 'file.png' + assert_conversion_does_not_apply URIChunk, 'file.ps' + end + + def test_uri_in_text + match(URIChunk, 'Go to: http://www.example.com/', :host => 'www.example.com', :path =>'/') + match(URIChunk, 'http://www.example.com/ is a link.', :host => 'www.example.com') + match(URIChunk, + 'Email david@loudthinking.com', + :scheme =>'mailto', :user =>'david', :host =>'loudthinking.com') + # check that trailing punctuation is not included in the hostname + match(URIChunk, 'Hey dude, http://fake.link.com.', :scheme => 'http', :host => 'fake.link.com') + # this is a textile link, no match please. + assert_conversion_does_not_apply(URIChunk, '"link":http://fake.link.com.') + end + + def test_uri_in_parentheses + match(URIChunk, 'URI (http://brackets.com.de) in brackets', :host => 'brackets.com.de') + match(URIChunk, 'because (as shown at research.net) the results', :host => 'research.net') + match(URIChunk, + 'A wiki (http://wiki.org/wiki.cgi?WhatIsWiki) page', + :scheme => 'http', :host => 'wiki.org', :path => '/wiki.cgi', :query => 'WhatIsWiki' + ) + end + + def test_uri_list_item + match( + URIChunk, + '* http://www.btinternet.com/~mail2minh/SonyEricssonP80xPlatform.sis', + :path => '/~mail2minh/SonyEricssonP80xPlatform.sis' + ) + end + + def test_interesting_uri_with__comma + # Counter-intuitively, this URL matches, but the query part includes the trailing comma. + # It has no way to know that the query does not include the comma. + match( + URIChunk, + "This text contains a URL http://someplace.org:8080/~person/stuff.cgi?arg=val, doesn't it?", + :scheme => 'http', :host => 'someplace.org', :port => '8080', :path => '/~person/stuff.cgi', + :query => 'arg=val,') + end + + def test_local_urls + # normal + match(LocalURIChunk, 'http://perforce:8001/toto.html', + :scheme => 'http', :host => 'perforce', + :port => '8001', :link_text => 'http://perforce:8001/toto.html') + + # in parentheses + match(LocalURIChunk, 'URI (http://localhost:2500) in brackets', + :host => 'localhost', :port => '2500') + match(LocalURIChunk, 'because (as shown at http://perforce:8001) the results', + :host => 'perforce', :port => '8001') + match(LocalURIChunk, + 'A wiki (http://localhost:2500/wiki.cgi?WhatIsWiki) page', + :scheme => 'http', :host => 'localhost', :path => '/wiki.cgi', + :port => '2500', :query => 'WhatIsWiki') + end + + def assert_conversion_does_not_apply(chunk_type, str) + processed_str = ContentStub.new(str.dup) + chunk_type.apply_to(processed_str) + assert_equal(str, processed_str) + end + +end diff --git a/test/unit/web_test.rb b/test/unit/web_test.rb new file mode 100644 index 00000000..62c3935e --- /dev/null +++ b/test/unit/web_test.rb @@ -0,0 +1,105 @@ +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') + +class WebTest < Test::Unit::TestCase + fixtures :webs, :pages, :revisions, :system, :wiki_references + + def setup + @web = webs(:instiki) + end + + def test_pages_by_revision + add_sample_pages + assert_equal 'EverBeenHated', @web.select.by_revision.first.name + end + + def test_pages_by_match + add_sample_pages + assert_equal 2, @web.select { |page| page.content =~ /me/i }.length + assert_equal 1, @web.select { |page| page.content =~ /Who/i }.length + assert_equal 0, @web.select { |page| page.content =~ /none/i }.length + end + + def test_002_references + add_sample_pages + assert_equal 1, @web.select.pages_that_reference('EverBeenHated').length + assert_equal 0, @web.select.pages_that_reference('EverBeenInLove').length + end + + def test_delete + add_sample_pages + assert_equal 2, @web.pages.length + @web.remove_pages([ @web.page('EverBeenInLove') ]) + assert_equal 1, @web.pages(true).length + end + + def test_initialize + web = Web.new(:name => 'Wiki2', :address => 'wiki2', :password => '123') + + assert_equal 'Wiki2', web.name + assert_equal 'wiki2', web.address + assert_equal '123', web.password + + # new web should be set for maximum features enabled + assert_equal :textile, web.markup + assert_equal '008B26', web.color + assert !web.safe_mode? + assert_equal([], web.pages) + assert web.allow_uploads? + assert_nil web.additional_style + assert !web.published? + assert !web.brackets_only? + assert !web.count_pages? + assert_equal 100, web.max_upload_size + end + + def test_initialize_invalid_name + assert_raises(Instiki::ValidationError) { + Web.create(:name => 'Wiki2', :address => "wiki\234", :password => '123') + } + end + + def test_new_page_linked_from_mother_page + # this was a bug in revision 204 + home = @web.add_page('HomePage', 'This page refers to AnotherPage', + Time.local(2004, 4, 4, 16, 50), 'Alexey Verkhovsky', test_renderer) + @web.add_page('AnotherPage', 'This is \AnotherPage', + Time.local(2004, 4, 4, 16, 51), 'Alexey Verkhovsky', test_renderer) + + @web.pages(true) + assert_equal [home], @web.select.pages_that_link_to('AnotherPage') + end + + def test_001_orphaned_pages + add_sample_pages + home = @web.add_page('HomePage', + 'This is a home page, it should not be an orphan', + Time.local(2004, 4, 4, 16, 50), 'AlexeyVerkhovsky', test_renderer) + author = @web.add_page('AlexeyVerkhovsky', + 'This is an author page, it should not be an orphan', + Time.local(2004, 4, 4, 16, 50), 'AlexeyVerkhovsky', test_renderer) + self_linked = @web.add_page('SelfLinked', + 'I am SelfLinked and link to EverBeenInLove', + Time.local(2004, 4, 4, 16, 50), 'AnonymousCoward', test_renderer) + + # page that links to itself, and nobody else links to it must be an orphan + assert_equal ['EverBeenHated', 'SelfLinked'], + @web.select.orphaned_pages.collect{ |page| page.name }.sort + end + + def test_page_names_by_author + page_names_by_author = webs(:test_wiki).page_names_by_author + assert_equal %w(AnAuthor DavidHeinemeierHansson Guest Me TreeHugger), + page_names_by_author.keys.sort + assert_equal %w(FirstPage HomePage), page_names_by_author['DavidHeinemeierHansson'] + assert_equal %w(Oak), page_names_by_author['TreeHugger'] + end + + private + + def add_sample_pages + @in_love = @web.add_page('EverBeenInLove', 'Who am I me', + Time.local(2004, 4, 4, 16, 50), 'DavidHeinemeierHansson', test_renderer) + @hated = @web.add_page('EverBeenHated', 'I am me EverBeenHated', + Time.local(2004, 4, 4, 16, 51), 'DavidHeinemeierHansson', test_renderer) + end +end diff --git a/test/unit/wiki_file_test.rb b/test/unit/wiki_file_test.rb new file mode 100644 index 00000000..233a73fa --- /dev/null +++ b/test/unit/wiki_file_test.rb @@ -0,0 +1,84 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'fileutils' + +class WikiFileTest < Test::Unit::TestCase + include FileUtils + fixtures :webs, :pages, :revisions, :system, :wiki_references + + def setup + @web = webs(:test_wiki) + mkdir_p("#{RAILS_ROOT}/public/wiki1/files/") + rm_rf("#{RAILS_ROOT}/public/wiki1/files/*") + WikiFile.delete_all + end + + def test_basic_store_and_retrieve_ascii_file + @web.wiki_files.create(:file_name => 'binary_file', :description => 'Binary file', :content => "\001\002\003") + binary = WikiFile.find_by_file_name('binary_file') + assert_equal "\001\002\003", binary.content + end + + def test_basic_store_and_retrieve_binary_file + @web.wiki_files.create(:file_name => 'text_file', :description => 'Text file', :content => "abc") + text = WikiFile.find_by_file_name('text_file') + assert_equal "abc", text.content + end + + def test_storing_an_image + rails_gif = File.open("#{RAILS_ROOT}/test/fixtures/rails.gif", 'rb') { |f| f.read } + assert_equal rails_gif.size, File.size("#{RAILS_ROOT}/test/fixtures/rails.gif") + + @web.wiki_files.create(:file_name => 'rails.gif', :description => 'Rails logo', :content => rails_gif) + + rails_gif_from_db = WikiFile.find_by_file_name('rails.gif') + assert_equal rails_gif.size, rails_gif_from_db.content.size + assert_equal rails_gif, rails_gif_from_db.content + end + + def test_mandatory_fields_validations + assert_validation(:file_name, '', :fail) + assert_validation(:file_name, nil, :fail) + assert_validation(:content, '', :fail) + assert_validation(:content, nil, :fail) + end + + def test_upload_size_validation + assert_validation(:content, 'a' * 100.kilobytes, :pass) + assert_validation(:content, 'a' * (100.kilobytes + 1), :fail) + end + + def test_file_name_size_validation + assert_validation(:file_name, '', :fail) + assert_validation(:file_name, 'a', :pass) + assert_validation(:file_name, 'a' * 50, :pass) + assert_validation(:file_name, 'a' * 51, :fail) + end + + def test_file_name_pattern_validation + assert_validation(:file_name, ".. Accep-table File.name", :pass) + assert_validation(:file_name, "/bad", :fail) + assert_validation(:file_name, "~bad", :fail) + assert_validation(:file_name, "..\bad", :fail) + assert_validation(:file_name, "\001bad", :fail) + assert_validation(:file_name, ".", :fail) + assert_validation(:file_name, "..", :fail) + end + + def test_find_by_file_name + assert_equal @file1, WikiFile.find_by_file_name('file1.txt') + assert_nil WikiFile.find_by_file_name('unknown_file') + end + + def assert_validation(field, value, expected_result) + values = {:file_name => '0', :description => '0', :content => '0'} + raise "WikiFile has no attribute named #{field.inspect}" unless values.has_key?(field) + values[field] = value + + new_object = @web.wiki_files.create(values) + if expected_result == :pass then assert(new_object.valid?, new_object.errors.inspect) + elsif expected_result == :fail then assert(!new_object.valid?) + else raise "Unknown value of expected_result: #{expected_result.inspect}" + end + end + +end diff --git a/test/unit/wiki_words_test.rb b/test/unit/wiki_words_test.rb new file mode 100755 index 00000000..f90a8d12 --- /dev/null +++ b/test/unit/wiki_words_test.rb @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') +require 'wiki_words' + +class WikiWordsTest < Test::Unit::TestCase + + def test_utf8_characters_in_wiki_word + assert_equal "Æåle Øen", WikiWords.separate("ÆåleØen") + assert_equal "ÆÅØle Øen", WikiWords.separate("ÆÅØleØen") + assert_equal "Æe ÅØle Øen", WikiWords.separate("ÆeÅØleØen") + assert_equal "Legetøj", WikiWords.separate("Legetøj") + end +end diff --git a/test/watir/e2e.rb b/test/watir/e2e.rb new file mode 100644 index 00000000..58cbdd20 --- /dev/null +++ b/test/watir/e2e.rb @@ -0,0 +1,370 @@ +require 'fileutils' +require 'cgi' +require 'test/unit' +require 'rexml/document' + +INSTIKI_ROOT = File.expand_path(File.dirname(__FILE__) + "/../..") +require(File.expand_path(File.dirname(__FILE__) + "/../../config/environment")) + +# TODO Create tests for: +# * exporting HTML +# * exporting markup +# * include tag + +# Use instiki/../watir, if such a directory exists; This can be a CVS HEAD version of Watir. +# Otherwise Watir has to be installed in ruby/lib. +$:.unshift INSTIKI_ROOT + '/../watir' if File.exists?(INSTIKI_ROOT + '/../watir/watir.rb') +require 'watir' + +INSTIKI_PORT = 2501 +HOME = "http://localhost:#{INSTIKI_PORT}" + +class E2EInstikiTest < Test::Unit::TestCase + + def startup + @@instiki = InstikiController.start + + sleep 8 + @@ie = Watir::IE.start(HOME) + @@ie.set_fast_speed if (ARGV & ['-d', '--demo', '-demo', 'demo']).empty? + + setup_web + setup_home_page + + @@ie + end + + def self.shutdown + @@ie.close if defined? @@ie + @@instiki.stop + end + + def ie + if defined? @@ie + @@ie + else + startup + end + end + + def setup + ie.goto HOME + ie + end + + # Numbers like _00010_ determine the sequence in which the test cases are executed by Test::Unit + # Unfortunately, this sequence is important. + + def test_00010_home_page_contents + check_main_menu + check_bottom_menu + check_footnote + end + + def test_00020_add_a_page + # Add reference to a non-existant wiki page + enter_markup('HomePage', '[[Another Wiki Page]]') + assert_equal '?', ie.link(:url, url(:new, 'Another Wiki Page')).text + + # Edit the first revision of a page + enter_markup('Another Wiki Page', 'First revision of Another Wiki Page, linked from HomePage') + + # Check contents of the new page + assert_equal url(:show, 'Another Wiki Page'), ie.url + assert_match /First revision of Another Wiki Page, linked from Home Page/, ie.text + assert_match /Linked from: Home Page/, ie.text + + # There must be three links to HomePage - main menu, contents of the page and navigation bar + links_to_homepage = ie.links.to_a.select { |link| link.text == 'Home Page' } + assert_equal 3, links_to_homepage.size + links_to_homepage.each { |link| assert_equal url(:show, 'HomePage'), link.href } + + # Check also the "created on ... by ..." footnote + assert_match Regexp.new('Created on ' + date_pattern + ' by Anonymous Coward\?'), ie.text + end + + def test_00030_edit_page + enter_markup('TestEditPage', 'Test Edit Page, revision 1') + assert_match /Test Edit Page, revision 1/, ie.text + + # subsequent revision by the anonymous author + enter_markup('TestEditPage', 'Test Edit Page, revision 1, altered') + assert_match /Test Edit Page, revision 1, altered/, ie.text + assert_match Regexp.new('Created on ' + date_pattern + ' by Anonymous Coward\?'), ie.text + + # revision by a named author + enter_markup('TestEditPage', 'Test Edit Page, revision 2', 'Author') + assert_match /Test Edit Page, revision 2/, ie.text + assert_match Regexp.new('Revised on ' + date_pattern + ' by Author\?'), ie.text + + link_to_previous_revision = ie.link(:name, 'to_previous_revision') + assert_equal url(:revision, 'TestEditPage', 1), link_to_previous_revision.href + assert_equal 'Back in time', link_to_previous_revision.text + assert_match /Edit \| Back in time \(1 revision\) \| See changes/, ie.text + + # another anonymous revision + enter_markup('TestEditPage', 'Test Edit Page, revision 3') + assert_match /Test Edit Page, revision 3/, ie.text + assert_match /Edit \| Back in time \(2 revisions\) \| See changes /, ie.text + end + + def test_00040_traversing_revisions + ie.goto url(:revision, 'TestEditPage', 2) + assert_match /Test Edit Page, revision 2/, ie.text + assert_match(Regexp.new( + 'Forward in time \(to current\) \| Back in time \(1 more\) \| See current \| See changes \| Rollback'), + ie.text) + + ie.link(:name, 'to_previous_revision').click + assert_match /Test Edit Page, revision 1, altered/, ie.text + assert_match /Forward in time \(2 more\) \| See current \| Rollback/, ie.text + + ie.link(:name, 'to_next_revision').click + assert_match /Test Edit Page, revision 2/, ie.text + + ie.link(:name, 'to_next_revision').click + assert_match /Test Edit Page, revision 3/, ie.text + end + + def test_00050_rollback + ie.goto url(:revision, 'TestEditPage', 2) + assert_match /Test Edit Page, revision 2/, ie.text + ie.link(:name, 'rollback').click + assert_equal url(:rollback, 'TestEditPage', 2), ie.url + assert_equal 'Test Edit Page, revision 2', ie.text_field(:name, 'content').value + + ie.text_field(:name, 'content').set('Test Edit Page, revision 2, rolled back') + ie.button(:value, 'Update').click + + assert_equal url(:show, 'TestEditPage'), ie.url + assert_match /Test Edit Page, revision 2, rolled back/, ie.text + end + + def test_0060_see_changes + ie.goto url(:show, 'TestEditPage') + + assert_match /Test Edit Page, revision 2, rolled back/, ie.text + + ie.link(:text, 'See changes').click + + assert_match /Showing changes from revision #2 to #3: Added \| Removed/, ie.text + assert_match /Test Edit Page, revision 22, rolled back/, ie.text + + ie.link(:text, 'Hide changes').click + + assert_match /Test Edit Page, revision 2, rolled back/, ie.text + end + + def test_0070_all_pages + # create a wanted page, and unlink Another Wiki Page from Home Page + # (to see that it doesn't show up in the orphans, regardless) + enter_markup('Another Wiki Page', 'Reference to a NonExistantPage') + + ie.link(:text, 'All Pages').click + + page_links = ie.links.map { |l| l.text } + expected_page_links = ['Another Wiki Page', 'Home Page', 'Test Edit Page', '?', + 'Another Wiki Page', 'Test Edit Page'] + assert_equal expected_page_links, page_links[-6..-1] + links_sequence = + 'All Pages.*Another Wiki Page.*Home Page.*Test Edit Page.*' + + 'Wanted Pages.*Non Existant Page\? wanted by Another Wiki Page.*'+ + 'Orphaned Pages.*Test Edit Page.*' + assert_match Regexp.new(links_sequence, Regexp::MULTILINE), ie.text + # and before that, we have the tail of the main menu + + +require 'breakpoint'; breakpoint + + + assert_equal 'Export', page_links[-7] + end + + def test_0080_recently_revised + ie.link(:text, 'Recently Revised').click + + links = ie.links.map { |l| l.text } + assert_equal ['Another Wiki Page', '?', 'Test Edit Page', '?', 'Home Page', '?'], links[-6..-1] + + expected_text = + 'Another Wiki Page.*by Anonymous Coward\?.*' + + 'Test Edit Page.*by Anonymous Coward\?.*' + + 'Home Page.*by Anonymous Coward\?.*' + assert_match Regexp.new(expected_text, Regexp::MULTILINE), ie.text + end + + def test_0090_authors + # create a revision of TestEditPage, and a corresponding author page + enter_markup('TestEditPage', '3rd revision of this page', 'Another Author') + ie.link(:afterText, 'Another Author').click + assert_equal url(:new, 'Another Author'), ie.url + enter_markup('Another Author', 'Email me at another_author@foo.bar.com', 'Another Author') + + ie.link(:text, 'Authors').click + + expected_authors = + 'Anonymous Coward\? co- or authored: Another Wiki Page, Home Page, Test Edit Page.*' + + 'Another Author co- or authored: Another Author, Test Edit Page.*' + + 'Author\? co- or authored: Test Edit Page' + assert_match Regexp.new(expected_authors, Regexp::MULTILINE), ie.text + + ie.link(:text, 'Another Author').click + assert_equal url(:show, 'Another Author'), ie.url + ie.back + ie.link(:text, 'Test Edit Page').click + assert_equal url(:show, 'TestEditPage'), ie.url + end + + def test_0100_feeds + ie.link(:text, 'Feeds').click + assert_equal url(:rss_with_content), ie.link(:text, 'Full content (RSS 2.0)').href + assert_equal url(:rss_with_headlines), ie.link(:text, 'Headlines (RSS 2.0)').href + + ie.link(:text, 'Full content (RSS 2.0)').click + assert_nothing_raised { REXML::Document.new ie.text } + + ie.back + ie.link(:text, 'Headlines (RSS 2.0)').click + assert_nothing_raised { REXML::Document.new ie.text } + end + + private + + def bp + require 'breakpoint' + breakpoint + end + + def check_main_menu + assert_equal HOME + '/wiki/list', ie.link(:text, 'All Pages').href + assert_equal HOME + '/wiki/recently_revised', ie.link(:text, 'Recently Revised').href + assert_equal HOME + '/wiki/authors', ie.link(:text, 'Authors').href + assert_equal HOME + '/wiki/feeds', ie.link(:text, 'Feeds').href + assert_equal HOME + '/wiki/export', ie.link(:text, 'Export').href + end + + def check_bottom_menu + assert_equal url(:edit, 'HomePage'), ie.link(:text, 'Edit Page').href + assert_equal HOME + '/wiki/edit_web', ie.link(:text, 'Edit Web').href + assert_equal url(:print, 'HomePage'), ie.link(:text, 'Print').href + end + + def check_footnote + assert_match /This site is running on Instiki/, ie.text + assert_equal 'http://instiki.org/', ie.link(:text, 'Instiki').href + assert_match /Powered by Ruby on Rails/, ie.text + assert_equal 'http://rubyonrails.com/', ie.link(:text, 'Ruby on Rails').href + end + + def date_pattern + '(January|February|March|April|May|June|July|August|September|October|November|December) ' + + '\d\d?, \d\d\d\d \d\d:\d\d:\d\d' + end + + def enter_markup(page, content, author = nil) + ie.goto url(:show, page) + if ie.url == url(:show, page) + ie.link(:name, 'edit').click + assert_equal url(:edit, page), ie.url + else + assert_equal url(:new, page), ie.url + end + + ie.text_field(:name, 'content').set(content) + ie.text_field(:name, 'author').set(author || '') + ie.button(:value, 'Submit').click + + assert_equal url(:show, page), ie.url + end + + def setup_web + assert_equal 'Wiki', ie.text_field(:name, 'web_name').value + assert_equal 'wiki', ie.text_field(:name, 'web_address').value + assert_equal '', ie.text_field(:name, 'password').value + assert_equal '', ie.text_field(:name, 'password_check').value + + ie.text_field(:name, 'password').set('123') + ie.text_field(:name, 'password_check').set('123') + ie.button(:value, 'Setup').click + assert_equal url(:new, 'HomePage'), ie.url + end + + def setup_home_page + ie.text_field(:name, 'content').set('Homepage of a test wiki') + ie.button(:value, 'Submit').click + assert_equal url(:show, 'HomePage'), ie.url + end + + def url(operation, page_name = nil, revision = nil) + page_name = CGI.escape(page_name) if page_name + case operation + when :edit, :new, :show, :print, :revision, :rollback + "#{HOME}/wiki/#{operation}/#{page_name}" + (revision ? "?rev=#{revision}" : '') + when :rss_with_content, :rss_with_headlines + "#{HOME}/wiki/#{operation}" + else + raise "Unsupported operation: '#{operation}" + end + end + +end + +class InstikiController + + attr_reader :process_id + + def self.start + startup_info = [68].pack('lx64') + process_info = [0, 0, 0, 0].pack('llll') + + clear_database + startup_command = + "ruby #{RAILS_ROOT}/instiki.rb --port #{INSTIKI_PORT} --environment development" + + result = Win32API.new('kernel32.dll', 'CreateProcess', 'pplllllppp', 'L').call( + nil, + startup_command, + 0, 0, 1, 0, 0, '.', startup_info, process_info) + + # TODO print the error code, or better yet a text message + raise "Failed to start Instiki." if result == 0 + + process_id = process_info.unpack('llll')[2] + return self.new(process_id) + end + + def self.clear_database + ENV['RAILS_ENV'] = 'development' + require INSTIKI_ROOT + '/config/environment.rb' + [WikiReference, Revision, Page, WikiFile, Web, System].each { |entity| entity.delete_all } + end + + def initialize(pid) + @process_id = pid + end + + def stop + right_to_terminate_process = 1 + handle = Win32API.new('kernel32.dll', 'OpenProcess', 'lil', 'l').call( + right_to_terminate_process, 0, @process_id) + Win32API.new('kernel32.dll', 'TerminateProcess', 'll', 'L').call(handle, 0) + end + +end + +begin + require 'test/unit/ui/console/testrunner' + Test::Unit::UI::Console::TestRunner.new(E2EInstikiTest.suite).start +rescue => e + $stderr.puts 'Unhandled error during test execution' + $stderr.puts e.message + $stderr.puts e.backtrace +ensure + begin + E2EInstikiTest::shutdown + rescue => e + $stderr.puts 'Error during shutdown' + $stderr.puts e.message + $stderr.puts e.backtrace + end +end diff --git a/vendor/plugins/dnsbl_check/README b/vendor/plugins/dnsbl_check/README new file mode 100644 index 00000000..dcbfb8d7 --- /dev/null +++ b/vendor/plugins/dnsbl_check/README @@ -0,0 +1,35 @@ +This plugin checks if the client is listed in RBLs (Real-time Blackhole Lists). +These are lists of IP addresses misbehaving. There are many RBLs, some are more +aggressive than others. More information at http://en.wikipedia.org/wiki/DNSBL + +This filter will result in one DNS request for every blocklist that you have +configured. This might be problematic for sites under heavy load, although this +plugin has been used on high-traffic sites without any problem. One DNS +request takes a few miliseconds to complete, after all. + + +INSTALLATION + +1. Download dnsbl_check-(version).tar.gz. You agree to the license. +2. Go to your application's 'vendor/plugins' directory +3. Untar (un-winzip) the above file: tar xvfz dnsbl_check.tar.gz +4. Restart your application. + + +VERSION HISTORY + +0.1 18 June 2006 Initial release +0.2 10 June 2006 Renamed to dnsbl_check, bugfix +0.3 20 June 2006 Removed sorbs from distribution, was not supposed to be included (too aggressive) +0.4 18 July 2006 Explicit return false added, moved to a per-controller basis (not global anymore) +1.0 16 August 2006 Renamed 0.4 to 1.0. I have been using the plugin very succesfully for months now. +1.1 17 October 2006 Multithreaded version +1.2 23 October 2006 Using the native Ruby resolver library for better multithreaded support +1.2.1 25 October 2006 Accepts a wider range of dns responses +1.2.2 11 December 2006 dnsbls are seemingly under attack, added code to cope with failing service + + +MORE INFORMATION + +http://spacebabies.nl/dnsbl_check/ +joost@spacebabies.nl diff --git a/vendor/plugins/dnsbl_check/init.rb b/vendor/plugins/dnsbl_check/init.rb new file mode 100644 index 00000000..19da77fd --- /dev/null +++ b/vendor/plugins/dnsbl_check/init.rb @@ -0,0 +1 @@ +ActionController::Base.send :include, DNSBL_Check diff --git a/vendor/plugins/dnsbl_check/lib/dnsbl_check.rb b/vendor/plugins/dnsbl_check/lib/dnsbl_check.rb new file mode 100644 index 00000000..b891aa8b --- /dev/null +++ b/vendor/plugins/dnsbl_check/lib/dnsbl_check.rb @@ -0,0 +1,58 @@ +# This plugin checks if the client is listed in DNSBLs (DNS Blackhole Lists). +# These are lists of IP addresses misbehaving. There are many DNSBLs, some are more +# aggressive than others. More information at http://en.wikipedia.org/wiki/DNSBL +# +# This plugin will perform one DNS request per client per blocklist. +# This plugin will deny service to clients those blocklists have listed. +# Whether any of this is acceptable is up to you. +# +# mailto:joost@spacebabies.nl +# License: MIT License, like Rails. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Version 1.2 +# http://www.spacebabies.nl/dnsbl_check +require 'resolv' + +module DNSBL_Check + $dnsbl_passed ||= [] + DNSBLS = %w{list.dsbl.org bl.spamcop.net sbl-xbl.spamhaus.org} + + private + # Filter to check if the client is listed. This will be run before all requests. + def dnsbl_check + return true if $dnsbl_passed.include? request.remote_addr + + passed = true + threads = [] + request.remote_addr =~ /(\d+).(\d+).(\d+).(\d+)/ + + # Check the remote address against each dnsbl in a separate thread + DNSBLS.each do |dnsbl| + threads << Thread.new("#$4.#$3.#$2.#$1.#{dnsbl}") do |host| + logger.warn("Checking DNSBL #{host}") + addr = Resolv.getaddress("#{host}") rescue '' + if addr[0,7]=="127.0.0" + logger.info("#{request.remote_addr} found using DNSBL #{host}") + passed = false + end + end + end + threads.each {|thread| thread.join(2)} # join threads, but use timeout to kill blocked ones + + # Add client ip to global passed cache if no dnsbls objected. else deny service. + if passed + $dnsbl_passed = $dnsbl_passed[0,49].unshift request.remote_addr + logger.warn("#{request.remote_addr} added to DNSBL passed cache") + else + render :text => 'Access denied', :status => 403 + return false + end + end +end diff --git a/vendor/plugins/rubyzip-0.9.1/ChangeLog b/vendor/plugins/rubyzip-0.9.1/ChangeLog new file mode 100644 index 00000000..1e281443 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/ChangeLog @@ -0,0 +1,1081 @@ +2006-07-01 10:04 thomas + + * Rakefile: Don't autorequire zip/zip - autorequire is deprecated. + +2006-06-30 09:28 thomas + + * Rakefile: [no log message] + + * NEWS, lib/zip/zip.rb: Bumped version number and reformatted NEWS + a bit. + +2006-06-29 22:49 technorama + + * lib/zip/zip.rb, NEWS: documentation additions + +2006-04-30 06:25 technorama + + * TODO, lib/zip/zip.rb, test/ziptest.rb: add documentation and test + for new ZipFile::extract + + * lib/zip/zip.rb: add some of the API suggestions from sf.net + #1281314 + + * lib/zip/zip.rb: apply patch for bug #1446926 + + * lib/zip/zip.rb: apply patch for bug #1459902 + +2006-04-26 17:17 technorama + + * lib/zip/zip.rb: add ZipFile @restore_*, documentation update + +2006-04-07 21:13 technorama + + * test/: gentestfiles.rb, zipfilesystemtest.rb, ziptest.rb: + additional tests + +2006-03-28 04:11 technorama + + * lib/zip/zip.rb: start of unix_uid, unix_gid, restore_* support + + * lib/zip/zip.rb: follow_symlinks is now optional + + * lib/zip/zip.rb: add eof? methods + + * test/ziptest.rb: eof? tests + +2006-02-26 09:57 technorama + + * README: add to authors + + * TODO: [no log message] + +2006-02-25 12:04 thomas + + * lib/zip/zip.rb, test/ziptest.rb: Did away with ZipStreamableFile. + +2006-02-23 08:03 technorama + + * lib/zip/zip.rb: unix file permissions. symlink support. rework + ZipEntry and delegate classes. reduce memory usage during + decompression. + +2006-02-22 23:44 technorama + + * lib/zip/zipfilesystem.rb: update permissionInt for mkdir + +2006-02-04 10:42 thomas + + * lib/zip/: ioextras.rb, zip.rb: Merged patch from oss-ruby. + +2005-11-19 16:17 thomas + + * lib/zip/zip.rb: [no log message] + +2005-11-08 08:23 thomas + + * lib/zip/ioextras.rb: Accepted patch from oss-ruby + +2005-10-07 09:54 thomas + + * TODO: [no log message] + +2005-09-06 21:19 thomas + + * lib/zip/zip.rb: [no log message] + + * NEWS: [no log message] + + * lib/zip/zip.rb, test/gentestfiles.rb, test/ziptest.rb: Fixed + problem on windows - tempfile has to be set to binmode again when + it is reopened + +2005-09-04 16:45 thomas + + * Rakefile: [no log message] + + * TODO: [no log message] + + * test/ziptest.rb: [no log message] + +2005-09-03 10:27 thomas + + * NEWS: [no log message] + + * TODO, lib/zip/zip.rb: [no log message] + + * lib/zip/ioextras.rb, lib/zip/zip.rb, test/ziptest.rb: Merged + patch from oss-ruby at technorama.net + + * test/ziptest.rb: Added failing test that shows that read and gets + don't mix currently + +2005-08-29 08:50 thomas + + * lib/zip/: ioextras.rb, zip.rb: [no log message] + + * NEWS, lib/zip/zip.rb: [no log message] + + * lib/zip/zip.rb: [no log message] + + * lib/zip/zip.rb: [no log message] + +2005-08-07 14:27 thomas + + * lib/zip/zip.rb, NEWS: [no log message] + +2005-08-06 11:12 thomas + + * lib/zip/: ioextras.rb, zip.rb: [no log message] + +2005-08-03 18:54 thomas + + * lib/zip/zip.rb: Read/write in chunks to preserve memory + +2005-07-02 15:08 thomas + + * lib/zip/zip.rb: Applied received patch concerning FreeBSD 4.5 + issue + +2005-04-03 16:52 thomas + + * samples/.cvsignore: [no log message] + + * samples/: qtzip.rb, zipdialogui.ui: Added a qt example + +2005-03-31 21:58 thomas + + * lib/zip/zip.rb, test/ziptest.rb: [no log message] + + * test/zipfilesystemtest.rb: [no log message] + +2005-03-17 18:17 thomas + + * Rakefile: [no log message] + + * NEWS, README, lib/zip/zip.rb: [no log message] + + * install.rb: Fixed install.rb + +2005-03-03 18:38 thomas + + * Rakefile: [no log message] + +2005-02-27 16:23 thomas + + * lib/zip/ziprequire.rb: Added documentation to ziprequire + + * README, TODO, lib/zip/ziprequire.rb: Added documentation to + ziprequire + + * Rakefile, test/ziptest.rb: [no log message] + +2005-02-19 21:30 thomas + + * lib/zip/ioextras.rb, lib/zip/stdrubyext.rb, + lib/zip/tempfile_bugfixed.rb, lib/zip/zip.rb, + lib/zip/ziprequire.rb, test/ioextrastest.rb, + test/stdrubyexttest.rb, test/zipfilesystemtest.rb, + test/ziprequiretest.rb, test/ziptest.rb: Added more rdoc and + changed the remaining tests to Test::Unit + + * lib/zip/: ioextras.rb, zip.rb: Added documentation to + ZipInputStream and ZipOutputStream + +2005-02-18 10:27 thomas + + * README: [no log message] + +2005-02-17 23:21 thomas + + * README, Rakefile: Added ppackage (publish package) task to + Rakefile + + * README, Rakefile, TODO: Added pdoc (publish doc) task to Rakefile + + * README, Rakefile, TODO, lib/zip/stdrubyext.rb, lib/zip/zip.rb, + lib/zip/zipfilesystem.rb: Added a bunch of documentation + + * test/ziptest.rb: [no log message] + +2005-02-16 20:04 thomas + + * NEWS, README, Rakefile: Improved documentation and added rdoc + task to Rakefile + + * NEWS, Rakefile, lib/zip/zip.rb: [no log message] + + * Rakefile, samples/example.rb, samples/example_filesystem.rb, + samples/gtkRubyzip.rb, samples/write_simple.rb, + samples/zipfind.rb, test/.cvsignore, test/gentestfiles.rb: + Improvements to Rakefile + +2005-02-15 23:35 thomas + + * NEWS, TODO: [no log message] + + * Rakefile, rubyzip.gemspec: Now uses Rake to build gem + + * Rakefile: [no log message] + + * lib/zip/zip.rb, test/.cvsignore, test/ziptest.rb, NEWS: Fixed + compatibility issue with ruby 1.8.2. Migrated test suite to + Test::Unit + + * NEWS, lib/zip/ioextras.rb, lib/zip/stdrubyext.rb, + lib/zip/tempfile_bugfixed.rb, lib/zip/zip.rb, + lib/zip/zipfilesystem.rb, lib/zip/ziprequire.rb, test/.cvsignore, + test/file1.txt, test/file1.txt.deflatedData, test/file2.txt, + test/gentestfiles.rb, test/ioextrastest.rb, + test/notzippedruby.rb, test/rubycode.zip, test/rubycode2.zip, + test/stdrubyexttest.rb, test/testDirectory.bin, + test/zipWithDirs.zip, test/zipfilesystemtest.rb, + test/ziprequiretest.rb, test/ziptest.rb, test/data/.cvsignore, + test/data/file1.txt, test/data/file1.txt.deflatedData, + test/data/file2.txt, test/data/notzippedruby.rb, + test/data/rubycode.zip, test/data/rubycode2.zip, + test/data/testDirectory.bin, test/data/zipWithDirs.zip: Changed + directory structure + +2005-02-13 22:44 thomas + + * Rakefile, TODO: [no log message] + + * rubyzip.gemspec: [no log message] + + * install.rb: Made install.rb independent of the current path + (fixes bug reported by Drew Robinson) + +2004-12-12 11:22 thomas + + * NEWS, TODO, samples/write_simple.rb: Fixed 'version needed to + extract'-field wrong in local headers + +2004-05-02 15:17 thomas + + * rubyzip.gemspec: Added gemspec contributed by Chad Fowler + +2004-04-02 07:25 thomas + + * NEWS: Fix for FreeBSD 4.9 + +2004-03-29 00:28 thomas + + * NEWS: [no log message] + +2004-03-28 17:59 thomas + + * NEWS: [no log message] + +2004-03-27 16:09 thomas + + * test/stdrubyexttest.rb: Patch for stdrubyext.rb from Nobu Nakada + + * test/: ioextrastest.rb, stdrubyexttest.rb: converted some files + to unix line-endings + +2004-03-25 16:34 thomas + + * NEWS, install.rb: Significantly reduced memory footprint when + modifying zip files + +2004-03-16 18:20 thomas + + * install.rb, test/alltests.rb, test/ioextrastest.rb, + test/stdrubyexttest.rb, test/ziptest.rb: IO utility classes moved + to new file ioextras.rb. Tests moved to new file ioextrastest.rb + +2004-02-27 13:21 thomas + + * NEWS: Optimization to avoid decompression and recompression + +2004-01-30 16:17 thomas + + * NEWS: [no log message] + + * README, test/zipfilesystemtest.rb, test/ziptest.rb: Applied + extra-field patch + +2003-12-13 16:57 thomas + + * TODO: [no log message] + +2003-12-10 00:25 thomas + + * test/ziptest.rb: (Temporary) fix to bug reported by Takashi Sano + +2003-08-23 09:42 thomas + + * test/ziptest.rb, NEWS: Fixed ZipFile.get_ouput_stream bug - data + was never written to zip + +2003-08-21 16:05 thomas + + * install.rb: [no log message] + + * alltests.rb, stdrubyexttest.rb, zipfilesystemtest.rb, + ziprequiretest.rb, ziptest.rb, test/alltests.rb, + test/stdrubyexttest.rb, test/zipfilesystemtest.rb, + test/ziprequiretest.rb, test/ziptest.rb: Moved all test ruby + files to test/ + + * NEWS, install.rb, stdrubyext.rb, stdrubyexttest.rb, zip.rb, + zipfilesystem.rb, zipfilesystemtest.rb, ziprequire.rb, + ziprequiretest.rb, ziptest.rb, samples/example.rb, + samples/example_filesystem.rb, samples/gtkRubyzip.rb, + samples/zipfind.rb: Moved all production source files to zip/ so + they are in the same dir as when they are installed + + * NEWS, TODO, alltests.rb: [no log message] + + * filearchive.rb, filearchivetest.rb, fileutils.rb: Removed + filearchive.rb, filearchivetest.rb and fileutils.rb + + * samples/.cvsignore, samples/example_filesystem.rb, zip.rb, + samples/example_filesystem.rb: Added + samples/example_filesystem.rb. Fixed Tempfile creation for + entries created with get_output_stream where entries were in a + subdirectory + + * zip.rb, ziptest.rb: Fixed mkdir bug. ZipFile.mkdir didn't work if + the zipfile doesn't exist already + + * ziptest.rb: [no log message] + + * TODO, zipfilesystemtest.rb: Globbing test placeholder commented + out + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented ZipFsDir.new + and open + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented DirFsIterator + and tests + +2003-08-20 22:50 thomas + + * NEWS, TODO: [no log message] + + * zipfilesystemtest.rb: [no log message] + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsDir.foreach, ZipFsDir.entries now reimplemented in terms of + it + + * README: [no log message] + + * zipfilesystem.rb, zipfilesystemtest.rb: [no log message] + + * zipfilesystem.rb: All access from ZipFsFile and ZipFsDir to + ZipFile is now routed through ZipFileNameMapper which has the + single responsibility of mapping entry/filenames + + * alltests.rb, stdrubyext.rb, stdrubyexttest.rb: Added + stdrubyexttest.rb and added test test_select_map + + * zipfilesystem.rb: ZipFsDir was in the wrong module. ZipFileSystem + now has a ctor that creates ZipFsDir and ZipFsFile instances, + instead of creating them lazily. It then passes the dir instance + to the file instance and vice versa + + * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: ZipFsFile.open + honours chdir + + * stdrubyext.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb, + ziptest.rb: Fixed ZipEntry::parent_as_string. Implemented + ZipFsDir.chdir, pwd and entries including test + +2003-08-19 15:44 thomas + + * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsDir.mkdir + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsDir.delete (and aliases rmdir and unlink) + + * zipfilesystem.rb, zipfilesystemtest.rb: Another dummy + implementation and commented out a test for select() which can be + added later + +2003-08-18 20:40 thomas + + * ziptest.rb: Honoured 1.8.0 Object.to_a deprecation warning + + * zip.rb, ziptest.rb, samples/example.rb, samples/zipfind.rb: + Converted a few more names to ruby underscore style that I missed + with the automated processing the first time around + + * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb, ziptest.rb: + Implemented Zip::ZipFile.get_output_stream + +2003-08-17 18:28 thomas + + * README, install.rb, stdrubyext.rb, zipfilesystem.rb, + zipfilesystemtest.rb: Updated README with Documentation section. + Updated install.rb. Fixed three tests that failed on 1.8.0. + +2003-08-14 05:40 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Added empty + implementations of atime and ctime + +2003-08-13 17:08 thomas + + * simpledist.rb: Moved simpledist to a separate repository called + 'misc' + + * NEWS: [no log message] + + * stdrubyext.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb, + ziprequire.rb, ziprequiretest.rb, ziptest.rb, samples/example.rb, + samples/gtkRubyzip.rb, samples/zipfind.rb: Changed all method + names to the ruby convention underscore style + + * alltests.rb, zipfilesystem.rb, zipfilesystemtest.rb: Implemented + a lot more of the stat methods. Mostly with dummy implementations + that return values that indicate that these features aren't + supported + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented more methods + and tests in zipfilesystem. Mostly empty methods as permissions + and file types other than files and directories are not supported + + * install.rb, stdrubyext.rb, zip.rb, zipfilesystem.rb, + zipfilesystemtest.rb: Addd file stdrubyext.rb and moved the + modifications to std ruby classes to it. Refactored the ZipFsStat + tests and ZipFsStat. Added Module.forwardMessages and used it to + implement the forwarding of calls in ZipFsStat + + * zipfilesystem.rb, zipfilesystemtest.rb: Added + Zip::ZipFsFile::ZipFsStat and started implementing it and its + methods + + * zipfilesystem.rb, zipfilesystemtest.rb, ziptest.rb: Updated and + added missing copyright notices + + * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: zipfilesystem.rb + is becoming big and not everyone will want to use that code. + Therefore zip.rb no longer requires it. Instead you must require + zipfilesystem.rb itself if you want to use it + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented dummy + permission test methods + + * TODO, zip.rb, ziptest.rb: Merged from patch from Kristoffer + Lunden. Fixed more 1.8.0 incompatibilites - tests run on 1.8.0 + now + +2003-08-12 19:18 thomas + + * zip.rb: Get rid of 1.8.0 warning + + * ziptest.rb: ruby 1.8.0 compatibility fix + + * NEWS, zip.rb: ruby-zlib 0.6.0 compatibility fix + +2002-12-22 20:12 thomas + + * zip.rb: [no log message] + +2002-09-16 22:11 thomas + + * NEWS: [no log message] + +2002-09-15 17:16 thomas + + * samples/zipfind.rb: [no log message] + + * samples/zipfind.rb: [no log message] + +2002-09-14 22:59 thomas + + * samples/zipfind.rb: Added simple zipfind script + +2002-09-13 23:53 thomas + + * TODO: Added TODO about openmode for zip entries binary/ascii + + * NEWS: ziptest now runs without errors with ruby-1.7.2-4 (Andy's + latest build) + + * zip.rb, ziprequiretest.rb, ziptest.rb: ziptest now runs without + errors with ruby-1.7.2-4 (Andy's latest build) + +2002-09-12 00:20 thomas + + * zipfilesystemtest.rb: Improved ZipFsFile.delete/unlink test + + * test/.cvsignore: [no log message] + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.delete/unlink + +2002-09-11 22:22 thomas + + * alltests.rb: [no log message] + + * NEWS, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: Fixed + AbstractInputStream.each_line ignored its aSeparator argument. + Implemented more ZipFsFile methods + + * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: ZipFileSystem is + now a module instead of a class, and is mixed into ZipFile, + instead of being made available as a property fileSystem + +2002-09-10 23:45 thomas + + * NEWS: Updated NEWS file + + * zip.rb: [no log message] + + * NEWS, zip.rb, ziptest.rb: Fix bug: rewind should reset lineno. + Fix bug: Deflater.read uses separate buffer from produceInput + (feeding gets/readline etc) + +2002-09-09 23:48 thomas + + * .cvsignore: [no log message] + +2002-09-09 22:55 uid26649 + + * zip.rb, ziptest.rb: Implemented ZipInputStream.rewind and + AbstractInputStream.lineno. Tests for both + +2002-09-09 20:31 thomas + + * zip.rb, ziptest.rb: ZipInputStream and ZipOutstream (thru their + AbstractInputStream and AbstractOutputStream now lie about being + kind_of?(IO) + +2002-09-08 16:38 thomas + + * zipfilesystemtest.rb: [no log message] + + * filearchive.rb, filearchivetest.rb, zip.rb, ziptest.rb: Moved + String additions from filearchive.rb to zip.rb (and moved tests + along too to ziptest.rb). Added ZipEntry.parentAsString and + ZipEntrySet.parent + + * ziptest.rb: Implemented ZipEntrySetTest.testDup and testCompound + + * TODO, zip.rb, ziptest.rb: Replaced Array with EntrySet for + keeping entries in a zip file. Tagged repository before this + commit, so this change can be rolled back, if it stinks + +2002-09-07 20:21 thomas + + * zip.rb, ziptest.rb: Implemented ZipEntry.<=> + + * ziptest.rb: Removed unused code + +2002-08-11 15:14 thomas + + * zip.rb, ziptest.rb: Made some changes to accomodate ruby 1.7.2 + +2002-07-27 15:25 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented ZipFsFile.new + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.pipe + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.link + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.symlink + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.readlink, wrapped ZipFileSystem class in Zip module + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.zero? + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented test for + ZipFsFile.directory? + +2002-07-26 23:56 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.socket? + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.join + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.ftype + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.blockdev? + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.size? (slightly different from size) + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.split + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.symlink? + + * alltests.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: + Implemented ZipFsFile.mtime + + * zipfilesystem.rb, zipfilesystemtest.rb: Implement ZipFsFile.file? + + * zip.rb, ziptest.rb: Implemented ZipEntry.file? + + * alltests.rb, filearchive.rb, filearchivetest.rb, zip.rb, + zipfilesystem.rb, zipfilesystemtest.rb, ziprequire.rb, + ziptest.rb: Implemented ZipFileSystem::ZipFsFile.size + + * zipfilesystem.rb, zipfilesystemtest.rb: [no log message] + + * test/zipWithDirs.zip: Changed zipWithDirs.zip so all the entries + in it have unix file endings + + * alltests.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: + Started implementing ZipFileSystem + + * test/zipWithDirs.zip: Added a zip file for testing with a + directory structure + +2002-07-22 21:40 thomas + + * TODO: [no log message] + + * TODO: [no log message] + +2002-07-21 18:20 thomas + + * NEWS: [no log message] + + * TODO: Updated TODO with a refactoring idea for FileArchive + + * filearchive.rb, filearchivetest.rb: Added some FileArchiveAdd + tests and cleaned up some of the FileArchive tests. extract and + add now have individual test fixtures. + + * filearchive.rb, filearchivetest.rb: Added tests for extract + called with regex src arg and Enumerable src arg + + * filearchivetest.rb: Added test for continueOnExistsProc when + extracting from a file archive + +2002-07-20 17:13 thomas + + * TODO, filearchivetest.rb, fileutils.rb, ziptest.rb, + test/.cvsignore: Added (failing) tests for FileArchive.add, added + code for creating test files for FileArchive.add tests. Added + fileutils.rb, which is borrowed from ruby 1.7.2 + + * filearchive.rb, filearchivetest.rb: [no log message] + + * filearchivetest.rb: Added tests for String extensions + + * alltests.rb, ziprequiretest.rb, ziptest.rb: [no log message] + + * install.rb: [no log message] + + * TODO: Updated TODO + + * filearchive.rb, filearchivetest.rb: All FileArchive.extract tests + run + +2002-07-19 23:11 thomas + + * filearchive.rb, filearchivetest.rb: [no log message] + + * filearchivetest.rb: [no log message] + + * filearchive.rb, filearchivetest.rb: [no log message] + + * filearchive.rb, filearchivetest.rb, zip.rb: [no log message] + +2002-07-08 13:41 thomas + + * TODO: [no log message] + +2002-06-11 19:47 thomas + + * filearchive.rb, filearchivetest.rb, zip.rb, ziptest.rb: [no log + message] + +2002-05-25 00:41 thomas + + * simpledist.rb: Added hackish script for creating dist files + +2002-04-30 21:22 thomas + + * TODO: [no log message] + + * filearchive.rb, filearchivetest.rb: [no log message] + + * filearchive.rb, filearchivetest.rb: Improved testing and wrote + some of the skeleton of extract. Still to do: Fix glob, so it + returns a hashmap instead of a list. The map will need to map the + full entry name to the last part of the name (which is only + really interesting for recursively extracted entries, otherwise + it is just the name). Glob.expandPathList should also output + directories with a trailing slash, which is doesn't right now. + + * filearchive.rb, filearchivetest.rb: Implemented the first few + tests for FileArchive + +2002-04-24 22:06 thomas + + * ziprequire.rb, ziprequiretest.rb: Appended copyright message to + ziprequire.rb and ziprequiretest.rb + + * zip.rb: Made ZipEntry tolerate invalid dates + +2002-04-21 00:57 thomas + + * NEWS, TODO, zip.rb, ziptest.rb: Read and write entry modification + date/time correctly + +2002-04-20 02:44 thomas + + * ziprequiretest.rb, test/rubycode2.zip: improved ZipRequireTest + + * ziprequire.rb: Made a warning go away + + * ziprequire.rb, ziprequiretest.rb, test/notzippedruby.rb, + test/rubycode.zip: Fixed a bug in ziprequire. Added + ziprequiretest.rb and test data files + +2002-04-19 22:43 thomas + + * zip.rb, ziptest.rb: Added recursion support to Glob module + +2002-04-18 21:37 thomas + + * NEWS, TODO, zip.rb, ziptest.rb: Added Glob module and GlobTest + unit test suite. This module provides the functionality to expand + a 'glob pattern' given a list of files - Next step is to use this + module in ZipFile + +2002-04-01 22:55 thomas + + * NEWS: [no log message] + + * TODO, zip.rb, ziprequire.rb: Added ziprequire.rb which contains a + proof-of-concept implementation of a require implementation that + can load ruby modules from a zip file. Needs unit tests and + polish. + +2002-03-31 01:13 thomas + + * README: [no log message] + +2002-03-30 16:14 thomas + + * TODO: [no log message] + + * .cvsignore, README, zip.rb: Added rdoc markup (only #:nodoc:all + modifiers) to zip.rb. Made README 'RDoc compliant' + +2002-03-29 23:29 thomas + + * TODO: [no log message] + + * example.rb, samples/.cvsignore, samples/example.rb, + samples/gtkRubyzip.rb: Moved example.rb to samples/. Added + another sample gtkRubyzip.rb + + * NEWS, TODO, TODO: [no log message] + + * .cvsignore, file1.txt, file1.txt.deflatedData, testDirectory.bin, + ziptest.rb, test/.cvsignore, test/file1.txt, + test/file1.txt.deflatedData, test/file2.txt, + test/testDirectory.bin: Added test/ directory and moved the + manually created test data files into it. Changed ziptest.rb so + it runs in test/ directory + + * TODO: [no log message] + + * NEWS, zip.rb, ziptest.rb: Don't decompress and recompress zip + entries when changing zip file + + * zip.rb: Performance optimization: Only write new ZipFile, if it + has been changed. The test suite runs in half the time now. + +2002-03-28 22:12 thomas + + * TODO: [no log message] + +2002-03-23 17:31 thomas + + * TODO: [no log message] + +2002-03-22 22:47 thomas + + * NEWS: [no log message] + + * NEWS, TODO: [no log message] + + * ziptest.rb: Found the tests that didn't use blocks to make sure + input streams are closed as soon as they arent used anymore and + got rid of the GC.start + + * ziptest.rb: All tests run on windows ruby 1.6.6 + + * zip.rb, ziptest.rb: Windows fixes: Fixed ZipFile.initialize which + needed to open zipfile file in binary mode. Added another + workaround for the return value from File.open(name) where name + is the name of a directory - ruby returns different exceptions in + linux, win/cygwin and windows. A number of tests failed because + in windows you cant delete a file that is open. Fixed by changing + ziptest.rb to use ZipInputStream.getInputStream with blocks a few + places. There is a hack in CommanZipFileFixture.setup where the + GC is explicitly invoked. Should be fixed with blocks instead. + The only currently failing test fails because the test data + creation fails to add a comment to 4entry.zip, because echo eats + the remainder of the line including the pipe character and the + following zip -z 4 entry.zip command + +2002-03-21 22:18 thomas + + * NEWS: [no log message] + + * NEWS, README, TODO, install.rb: Added install.rb + + * ziptest.rb: [no log message] + + * NEWS, TODO: [no log message] + + * .cvsignore, TODO, zip.rb, ziptest.rb: Added + test_extractDirectoryExistsAsFileOverwrite and fixed to pass + + * zip.rb, ziptest.rb: Extraction of directory entries is now + supported + +2002-03-20 21:59 thomas + + * NEWS: [no log message] + + * COPYING, README, README.txt: Removed COPYING, renamed README.txt + to README. Updated README + + * example.rb: Fixed example.rb added example that shows zip file + manipulation with Zip::ZipFile + + * .cvsignore: [no log message] + + * TODO, zip.rb, ziptest.rb: Directories can now be added (not + recursively, the directory entry itself. Directories are + recognized by a empty entries with a trailing /. The purpose of + storing them explicitly in the zip file is to be able to store + permission and ownership information + + * TODO, zip.rb, ziptest.rb: zip.rb depended on ftools but it was + only included in ziptest.rb + + * zip.rb, ziptest.rb: ZipError is now a subclass of StandardError + instead of RuntimeError. ZipError now has several subclasses. + +2002-03-19 22:26 thomas + + * TODO: [no log message] + + * TODO, ziptest.rb: Unit test ZipFile.getInputStream with block + + * TODO, zip.rb, ziptest.rb: Unit test for adding new entry with + name that already exists in archive, and fixed to pass test + + * TODO, zip.rb, ziptest.rb: Added unit tests for rename to existing + entry + + * TODO: [no log message] + + * TODO, zip.rb, ziptest.rb: Unit test calling ZipFile.extract with + block + +2002-03-18 21:06 thomas + + * TODO: [no log message] + + * zip.rb, ziptest.rb: ZipFile#commit now reinitializes ZipFile. + + * TODO, zip.rb, ziptest.rb: Refactoring: + + Collapsed ZipEntry and ZipStreamableZipEntry into ZipEntry. + + Collapsed BasicZipFile and ZipFile into ZipFile. + + * zip.rb: Removed method that was never called + +2002-03-17 22:33 thomas + + * TODO: [no log message] + + * ziptest.rb: Run tests with =true as default + + * NEWS, TODO, zip.rb, ziptest.rb: Now runs with -w switch without + warnings + + * .cvsignore: [no log message] + + * zip.rb, ziptest.rb: Down to one failing test + + * zip.rb, ziptest.rb: [no log message] + + * TODO, zip.rb, ziptest.rb: [no log message] + +2002-02-25 19:42 thomas + + * TODO: Added more todos + +2002-02-23 15:51 thomas + + * zip.rb: [no log message] + + * zip.rb, ziptest.rb: [no log message] + + * zip.rb, ziptest.rb: [no log message] + +2002-02-03 18:47 thomas + + * ziptest.rb: [no log message] + +2002-02-02 15:58 thomas + + * example.rb, zip.rb, ziptest.rb: [no log message] + + * .cvsignore: [no log message] + + * example.rb, zip.rb, ziptest.rb: Renamed SimpleZipFile to + BasicZipFile + + * TODO: [no log message] + + * ziptest.rb: More test cases - all of them failing, so now there + are 18 failing test cases. Three more test cases to implement, + then it is time for the production code + +2002-02-01 21:49 thomas + + * ziptest.rb: [no log message] + + * ziptest.rb: Also run SimpleZipFile tests for ZipFile. + + * example.rb, zip.rb, ziptest.rb: ZipFile renamed to SimpleZipFile. + The new ZipFile will have many more methods that are useful for + managing archives. + +2002-01-29 20:30 thomas + + * TODO: [no log message] + +2002-01-26 00:18 thomas + + * NEWS: [no log message] + + * ziptest.rb: In unit test: work around ruby/cygwin weirdness. You + get an Errno::EEXISTS instead of an Errno::EISDIR if you try to + open a file for writing that is a directory. + + * ziptest.rb: Fixed test that failed on windows because of CRLF + line ending + +2002-01-25 23:58 thomas + + * ziptest.rb: [no log message] + + * .cvsignore, example.rb, zip.rb: Fixed bug reading from empty + deflated entry in zip file + + * .cvsignore: [no log message] + + * ziptest.rb: [no log message] + + * NEWS, README.txt, zip.rb, ziptest.rb: Zip write support is now + fully functional in the form of ZipOutputStream. + + * zip.rb, ziptest.rb: [no log message] + + * zip.rb, ziptest.rb: [no log message] + +2002-01-20 16:00 thomas + + * zip.rb, ziptest.rb: Added Deflater and DeflaterTest. + + * .cvsignore: [no log message] + + * .cvsignore: Added .cvsignore file + + * zip.rb, ziptest.rb: Added ZipEntry.writeCDirEntry and misc minor + fixes + +2002-01-19 23:28 thomas + + * example.rb, zip.rb, ziptest.rb: NOTICE: Not all tests run!! + + ZipOutputStream in progress + + Wrapped rubyzip in namespace module Zip. + +2002-01-17 18:52 thomas + + * ziptest.rb: Fail nicely if the user doesn't have info-zip + compatible zip in the path + +2002-01-10 18:02 thomas + + * zip.rb: Adjusted chunk size to 32k after a few perf measurements + +2002-01-09 22:10 thomas + + * README.txt: License now same as rubys, not just GPL + +2002-01-06 00:19 thomas + + * README.txt: [no log message] + +2002-01-05 23:09 thomas + + * NEWS, README.txt, NEWS: Updated NEWS file + + * README.txt, zip.rb, ziptest.rb, zlib.c.diff: Added tests for + decompressors and a tests for ZipLocalEntry, + ZipCentralDirectoryEntry and ZipCentralDirectory for handling of + corrupt data + + * file1.txt.deflatedData: deflated data extracted from a zip file. + contains file1.txt + + * zip.rb: Changed references to Inflate to Zlib::inflate for + compatibility with ruby-zlib-0.5 + + * README.txt, zip.rb, ziptest.rb: [no log message] + + * example.rb, NEWS: [no log message] + + * COPYING, README.txt: [no log message] + + * ziptest.rb: Fixed problem with test file creation + + * README.txt: Updated README.txt + + * zip.rb, ziptest.rb: ZipFile now works + +2002-01-04 21:51 thomas + + * testDirectory.bin, zip.rb, ziptest.rb: + ZipCentralDirectoryEntryTest now runs + + * ziptest.rb: Changed + ZIpLocalNEtryTest::test_ReadLocalEntryHeaderOfFirstTestZipEntry + so it works on both unix too. It only worked on windows because + the test made assumptions about the compressed size and crc of an + entry, but that differs depending on the OS because of the CRLF + thing. + + * README.txt: Added note about zlib.c patch + +2002-01-02 18:48 thomas + + * README.txt, example.rb, file1.txt, zip.rb, ziptest.rb, + zlib.c.diff: initial + + * README.txt, example.rb, file1.txt, zip.rb, ziptest.rb, + zlib.c.diff: Initial revision + diff --git a/vendor/plugins/rubyzip-0.9.1/NEWS b/vendor/plugins/rubyzip-0.9.1/NEWS new file mode 100644 index 00000000..0f46f727 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/NEWS @@ -0,0 +1,144 @@ += Version 0.9.1 + +Added symlink support and support for unix file permissions. Reduced +memory usage during decompression. + +New methods ZipFile::[follow_symlinks, restore_times, restore_permissions, restore_ownership]. +New methods ZipEntry::unix_perms, ZipInputStream::eof?. +Added documentation and test for new ZipFile::extract. +Added some of the API suggestions from sf.net #1281314. +Applied patch for sf.net bug #1446926. +Applied patch for sf.net bug #1459902. +Rework ZipEntry and delegate classes. + += Version 0.5.12 + +Fixed problem with writing binary content to a ZipFile in MS Windows. + += Version 0.5.11 + +Fixed name clash file method copy_stream from fileutils.rb. Fixed +problem with references to constant CHUNK_SIZE. +ZipInputStream/AbstractInputStream read is now buffered like ruby IO's +read method, which means that read and gets etc can be mixed. The +unbuffered read method has been renamed to sysread. + += Version 0.5.10 + +Fixed method name resolution problem with FileUtils::copy_stream and +IOExtras::copy_stream. + += Version 0.5.9 + +Fixed serious memory consumption issue + += Version 0.5.8 + +Fixed install script. + += Version 0.5.7 + +install.rb no longer assumes it is being run from the toplevel source +dir. Directory structure changed to reflect common ruby library +project structure. Migrated from RubyUnit to Test::Unit format. Now +uses Rake to build source packages and gems and run unit tests. + += Version 0.5.6 + +Fix for FreeBSD 4.9 which returns Errno::EFBIG instead of +Errno::EINVAL for some invalid seeks. Fixed 'version needed to +extract'-field incorrect in local headers. + += Version 0.5.5 + +Fix for a problem with writing zip files that concerns only ruby 1.8.1. + += Version 0.5.4 + +Significantly reduced memory footprint when modifying zip files. + += Version 0.5.3 + +Added optimization to avoid decompressing and recompressing individual +entries when modifying a zip archive. + += Version 0.5.2 + +Fixed ZipFile corruption bug in ZipFile class. Added basic unix +extra-field support. + += Version 0.5.1 + +Fixed ZipFile.get_output_stream bug. + += Version 0.5.0 + +List of changes: +* Ruby 1.8.0 and ruby-zlib 0.6.0 compatibility +* Changed method names from camelCase to rubys underscore style. +* Installs to zip/ subdir instead of directly to site_ruby +* Added ZipFile.directory and ZipFile.file - each method return an +object that can be used like Dir and File only for the contents of the +zip file. +* Added sample application zipfind which works like Find.find, only +Zip::ZipFind.find traverses into zip archives too. + +Bug fixes: +* AbstractInputStream.each_line with non-default separator + + += Version 0.5.0a + +Source reorganized. Added ziprequire, which can be used to load ruby +modules from a zip file, in a fashion similar to jar files in +Java. Added gtkRubyzip, another sample application. Implemented +ZipInputStream.lineno and ZipInputStream.rewind + +Bug fixes: + +* Read and write date and time information correctly for zip entries. +* Fixed read() using separate buffer, causing mix of gets/readline/read to +cause problems. + += Version 0.4.2 + +Performance optimizations. Test suite runs in half the time. + += Version 0.4.1 + +Windows compatibility fixes. + += Version 0.4.0 + +Zip::ZipFile is now mutable and provides a more convenient way of +modifying zip archives than Zip::ZipOutputStream. Operations for +adding, extracting, renaming, replacing and removing entries to zip +archives are now available. + +Runs without warnings with -w switch. + +Install script install.rb added. + + += Version 0.3.1 + +Rudimentary support for writing zip archives. + + += Version 0.2.2 + +Fixed and extended unit test suite. Updated to work with ruby/zlib +0.5. It doesn't work with earlier versions of ruby/zlib. + + += Version 0.2.0 + +Class ZipFile added. Where ZipInputStream is used to read the +individual entries in a zip file, ZipFile reads the central directory +in the zip archive, so you can get to any entry in the zip archive +without having to skipping through all the preceeding entries. + + += Version 0.1.0 + +First working version of ZipInputStream. diff --git a/vendor/plugins/rubyzip-0.9.1/README b/vendor/plugins/rubyzip-0.9.1/README new file mode 100644 index 00000000..81ec4c5d --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/README @@ -0,0 +1,72 @@ += rubyzip + +rubyzip is a ruby library for reading and writing zip files. + += Install + +If you have rubygems you can install rubyzip directly from the gem +repository + + gem install rubyzip + +Otherwise obtain the source (see below) and run + + ruby install.rb + +To run the unit tests you need to have test::unit installed + + rake test + + += Documentation + +There is more than one way to access or create a zip archive with +rubyzip. The basic API is modeled after the classes in +java.util.zip from the Java SDK. This means there are classes such +as Zip::ZipInputStream, Zip::ZipOutputStream and +Zip::ZipFile. Zip::ZipInputStream provides a basic interface for +iterating through the entries in a zip archive and reading from the +entries in the same way as from a regular File or IO +object. ZipOutputStream is the corresponding basic output +facility. Zip::ZipFile provides a mean for accessing the archives +central directory and provides means for accessing any entry without +having to iterate through the archive. Unlike Java's +java.util.zip.ZipFile rubyzip's Zip::ZipFile is mutable, which means +it can be used to change zip files as well. + +Another way to access a zip archive with rubyzip is to use rubyzip's +Zip::ZipFileSystem API. Using this API files can be read from and +written to the archive in much the same manner as ruby's builtin +classes allows files to be read from and written to the file system. + +rubyzip also features the +zip/ziprequire.rb[link:files/lib/zip/ziprequire_rb.html] module which +allows ruby to load ruby modules from zip archives. + +For details about the specific behaviour of classes and methods refer +to the test suite. Finally you can generate the rdoc documentation or +visit http://rubyzip.sourceforge.net. + += License + +rubyzip is distributed under the same license as ruby. See +http://www.ruby-lang.org/en/LICENSE.txt + + += Website and Project Home + +http://rubyzip.sourceforge.net + +http://sourceforge.net/projects/rubyzip + +== Download (tarballs and gems) + +http://sourceforge.net/project/showfiles.php?group_id=43107&package_id=35377 + += Authors + +Thomas Sondergaard (thomas at sondergaard.cc) + +Technorama Ltd. (oss-ruby-zip at technorama.net) + +extra-field support contributed by Tatsuki Sugiura (sugi at nemui.org) diff --git a/vendor/plugins/rubyzip-0.9.1/Rakefile b/vendor/plugins/rubyzip-0.9.1/Rakefile new file mode 100755 index 00000000..c6581cf6 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/Rakefile @@ -0,0 +1,110 @@ +# Rakefile for RubyGems -*- ruby -*- + +require 'rubygems' +require 'rake/clean' +require 'rake/testtask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/rdoctask' +require 'rake/contrib/sshpublisher' +require 'net/ftp' + +PKG_NAME = 'rubyzip' +PKG_VERSION = File.read('lib/zip/zip.rb').match(/\s+VERSION\s*=\s*'(.*)'/)[1] + +PKG_FILES = FileList.new + +PKG_FILES.add %w{ README NEWS TODO ChangeLog install.rb Rakefile } +PKG_FILES.add %w{ samples/*.rb } +PKG_FILES.add %w{ test/*.rb } +PKG_FILES.add %w{ test/data/* } +PKG_FILES.exclude "test/data/generated" +PKG_FILES.add %w{ lib/**/*.rb } + +def clobberFromCvsIgnore(path) + CLOBBER.add File.readlines(path+'/.cvsignore').map { + |f| File.join(path, f.chomp) + } rescue StandardError +end + +clobberFromCvsIgnore '.' +clobberFromCvsIgnore 'samples' +clobberFromCvsIgnore 'test' +clobberFromCvsIgnore 'test/data' + +task :default => [:test] + +desc "Run unit tests" +task :test do + ruby %{-C test alltests.rb} +end + +# Shortcuts for test targets +task :ut => [:test] + +spec = Gem::Specification.new do |s| + s.name = PKG_NAME + s.version = PKG_VERSION + s.author = "Thomas Sondergaard" + s.email = "thomas(at)sondergaard.cc" + s.homepage = "http://rubyzip.sourceforge.net/" + s.platform = Gem::Platform::RUBY + s.summary = "rubyzip is a ruby module for reading and writing zip files" + s.files = PKG_FILES.to_a + s.require_path = 'lib' +end + +Rake::GemPackageTask.new(spec) do |pkg| + pkg.need_zip = true + pkg.need_tar = true +end + +Rake::RDocTask.new do |rd| + rd.main = "README" + rd.rdoc_files.add %W{ lib/zip/*.rb README NEWS TODO ChangeLog } + rd.options << "--title 'rubyzip documentation' --webcvs http://cvs.sourceforge.net/viewcvs.py/rubyzip/rubyzip/" +# rd.options << "--all" +end + +desc "Publish documentation" +task :pdoc => [:rdoc] do + Rake::SshFreshDirPublisher. + new("thomas@rubyzip.sourceforge.net", "/home/groups/r/ru/rubyzip/htdocs", "html").upload +end + +desc "Publish package" +task :ppackage => [:package] do + Net::FTP.open("upload.sourceforge.net", + "ftp", + ENV['USER']+"@"+ENV['HOSTNAME']) { + |ftpclient| + ftpclient.passive = true + ftpclient.chdir "incoming" + Dir['pkg/*.{tgz,zip,gem}'].each { + |e| + ftpclient.putbinaryfile(e, File.basename(e)) + } + } +end + +desc "Generate the ChangeLog file" +task :ChangeLog do + puts "Updating ChangeLog" + system %{cvs2cl} +end + +desc "Make a release" +task :release => [:tag_release, :pdoc, :ppackage] do +end + +desc "Make a release tag" +task :tag_release do + tag = "release-#{PKG_VERSION.gsub('.','-')}" + + puts "Checking for tag '#{tag}'" + if (Regexp.new("^\\s+#{tag}") =~ `cvs log README`) + abort "Tag '#{tag}' already exists" + end + puts "Tagging module with '#{tag}'" + system("cvs tag #{tag}") +end diff --git a/vendor/plugins/rubyzip-0.9.1/TODO b/vendor/plugins/rubyzip-0.9.1/TODO new file mode 100644 index 00000000..e24cde57 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/TODO @@ -0,0 +1,16 @@ + +* ZipInputStream: Support zip-files with trailing data descriptors +* Adjust rdoc stylesheet to advertise inherited methods if possible +* Suggestion: Add ZipFile/ZipInputStream example that demonstrates extracting all entries. +* Suggestion: ZipFile#extract destination should default to "." +* Suggestion: ZipEntry should have extract(), get_input_stream() methods etc +* SUggestion: ZipInputStream/ZipOutputStream should accept an IO object in addition to a filename. +* (is buffering used anywhere with write?) +* Inflater.sysread should pass the buffer to produce_input. +* Implement ZipFsDir.glob +* ZipFile.checkIntegrity method +* non-MSDOS permission attributes +** See mail from Ned Konz to ruby-talk subj. "Re: SV: [ANN] Archive 0.2" +* Packager version, required unpacker version in zip headers +** See mail from Ned Konz to ruby-talk subj. "Re: SV: [ANN] Archive 0.2" +* implement storing attributes and ownership information diff --git a/vendor/plugins/rubyzip-0.9.1/install.rb b/vendor/plugins/rubyzip-0.9.1/install.rb new file mode 100755 index 00000000..405e2b0b --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/install.rb @@ -0,0 +1,22 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +require 'rbconfig' +require 'find' +require 'ftools' + +include Config + +files = %w{ stdrubyext.rb ioextras.rb zip.rb zipfilesystem.rb ziprequire.rb tempfile_bugfixed.rb } + +INSTALL_DIR = File.join(CONFIG["sitelibdir"], "zip") +File.makedirs(INSTALL_DIR) + +SOURCE_DIR = File.join(File.dirname($0), "lib/zip") + +files.each { + |filename| + installPath = File.join(INSTALL_DIR, filename) + File::install(File.join(SOURCE_DIR, filename), installPath, 0644, true) +} diff --git a/vendor/plugins/rubyzip-0.9.1/lib/zip/ioextras.rb b/vendor/plugins/rubyzip-0.9.1/lib/zip/ioextras.rb new file mode 100755 index 00000000..c458bb58 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/lib/zip/ioextras.rb @@ -0,0 +1,155 @@ +module IOExtras #:nodoc: + + CHUNK_SIZE = 32768 + + RANGE_ALL = 0..-1 + + def self.copy_stream(ostream, istream) + s = '' + ostream.write(istream.read(CHUNK_SIZE, s)) until istream.eof? + end + + + # Implements kind_of? in order to pretend to be an IO object + module FakeIO + def kind_of?(object) + object == IO || super + end + end + + # Implements many of the convenience methods of IO + # such as gets, getc, readline and readlines + # depends on: input_finished?, produce_input and read + module AbstractInputStream + include Enumerable + include FakeIO + + def initialize + super + @lineno = 0 + @outputBuffer = "" + end + + attr_accessor :lineno + + def read(numberOfBytes = nil, buf = nil) + tbuf = nil + + if @outputBuffer.length > 0 + if numberOfBytes <= @outputBuffer.length + tbuf = @outputBuffer.slice!(0, numberOfBytes) + else + numberOfBytes -= @outputBuffer.length if (numberOfBytes) + rbuf = sysread(numberOfBytes, buf) + tbuf = @outputBuffer + tbuf << rbuf if (rbuf) + @outputBuffer = "" + end + else + tbuf = sysread(numberOfBytes, buf) + end + + return nil unless (tbuf) + + if buf + buf.replace(tbuf) + else + buf = tbuf + end + + buf + end + + def readlines(aSepString = $/) + retVal = [] + each_line(aSepString) { |line| retVal << line } + return retVal + end + + def gets(aSepString=$/) + @lineno = @lineno.next + return read if aSepString == nil + aSepString="#{$/}#{$/}" if aSepString == "" + + bufferIndex=0 + while ((matchIndex = @outputBuffer.index(aSepString, bufferIndex)) == nil) + bufferIndex=@outputBuffer.length + if input_finished? + return @outputBuffer.empty? ? nil : flush + end + @outputBuffer << produce_input + end + sepIndex=matchIndex + aSepString.length + return @outputBuffer.slice!(0...sepIndex) + end + + def flush + retVal=@outputBuffer + @outputBuffer="" + return retVal + end + + def readline(aSepString = $/) + retVal = gets(aSepString) + raise EOFError if retVal == nil + return retVal + end + + def each_line(aSepString = $/) + while true + yield readline(aSepString) + end + rescue EOFError + end + + alias_method :each, :each_line + end + + + # Implements many of the output convenience methods of IO. + # relies on << + module AbstractOutputStream + include FakeIO + + def write(data) + self << data + data.to_s.length + end + + + def print(*params) + self << params.to_s << $\.to_s + end + + def printf(aFormatString, *params) + self << sprintf(aFormatString, *params) + end + + def putc(anObject) + self << case anObject + when Fixnum then anObject.chr + when String then anObject + else raise TypeError, "putc: Only Fixnum and String supported" + end + anObject + end + + def puts(*params) + params << "\n" if params.empty? + params.flatten.each { + |element| + val = element.to_s + self << val + self << "\n" unless val[-1,1] == "\n" + } + end + + end + +end # IOExtras namespace module + + + +# Copyright (C) 2002-2004 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/lib/zip/stdrubyext.rb b/vendor/plugins/rubyzip-0.9.1/lib/zip/stdrubyext.rb new file mode 100755 index 00000000..833365db --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/lib/zip/stdrubyext.rb @@ -0,0 +1,111 @@ +unless Enumerable.method_defined?(:inject) + module Enumerable #:nodoc:all + def inject(n = 0) + each { |value| n = yield(n, value) } + n + end + end +end + +module Enumerable #:nodoc:all + # returns a new array of all the return values not equal to nil + # This implementation could be faster + def select_map(&aProc) + map(&aProc).reject { |e| e.nil? } + end +end + +unless Object.method_defined?(:object_id) + class Object #:nodoc:all + # Using object_id which is the new thing, so we need + # to make that work in versions prior to 1.8.0 + alias object_id id + end +end + +unless File.respond_to?(:read) + class File # :nodoc:all + # singleton method read does not exist in 1.6.x + def self.read(fileName) + open(fileName) { |f| f.read } + end + end +end + +class String #:nodoc:all + def starts_with(aString) + rindex(aString, 0) == 0 + end + + def ends_with(aString) + index(aString, -aString.size) + end + + def ensure_end(aString) + ends_with(aString) ? self : self + aString + end + + def lchop + slice(1, length) + end +end + +class Time #:nodoc:all + + #MS-DOS File Date and Time format as used in Interrupt 21H Function 57H: + # + # Register CX, the Time: + # Bits 0-4 2 second increments (0-29) + # Bits 5-10 minutes (0-59) + # bits 11-15 hours (0-24) + # + # Register DX, the Date: + # Bits 0-4 day (1-31) + # bits 5-8 month (1-12) + # bits 9-15 year (four digit year minus 1980) + + + def to_binary_dos_time + (sec/2) + + (min << 5) + + (hour << 11) + end + + def to_binary_dos_date + (day) + + (month << 5) + + ((year - 1980) << 9) + end + + # Dos time is only stored with two seconds accuracy + def dos_equals(other) + to_i/2 == other.to_i/2 + end + + def self.parse_binary_dos_format(binaryDosDate, binaryDosTime) + second = 2 * ( 0b11111 & binaryDosTime) + minute = ( 0b11111100000 & binaryDosTime) >> 5 + hour = (0b1111100000000000 & binaryDosTime) >> 11 + day = ( 0b11111 & binaryDosDate) + month = ( 0b111100000 & binaryDosDate) >> 5 + year = ((0b1111111000000000 & binaryDosDate) >> 9) + 1980 + begin + return Time.local(year, month, day, hour, minute, second) + end + end +end + +class Module #:nodoc:all + def forward_message(forwarder, *messagesToForward) + methodDefs = messagesToForward.map { + |msg| + "def #{msg}; #{forwarder}(:#{msg}); end" + } + module_eval(methodDefs.join("\n")) + end +end + + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/lib/zip/tempfile_bugfixed.rb b/vendor/plugins/rubyzip-0.9.1/lib/zip/tempfile_bugfixed.rb new file mode 100755 index 00000000..6cf8e0af --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/lib/zip/tempfile_bugfixed.rb @@ -0,0 +1,195 @@ +# +# tempfile - manipulates temporary files +# +# $Id: tempfile_bugfixed.rb,v 1.2 2005/02/19 20:30:33 thomas Exp $ +# + +require 'delegate' +require 'tmpdir' + +module BugFix #:nodoc:all + +# A class for managing temporary files. This library is written to be +# thread safe. +class Tempfile < DelegateClass(File) + MAX_TRY = 10 + @@cleanlist = [] + + # Creates a temporary file of mode 0600 in the temporary directory + # whose name is basename.pid.n and opens with mode "w+". A Tempfile + # object works just like a File object. + # + # If tmpdir is omitted, the temporary directory is determined by + # Dir::tmpdir provided by 'tmpdir.rb'. + # When $SAFE > 0 and the given tmpdir is tainted, it uses + # /tmp. (Note that ENV values are tainted by default) + def initialize(basename, tmpdir=Dir::tmpdir) + if $SAFE > 0 and tmpdir.tainted? + tmpdir = '/tmp' + end + + lock = nil + n = failure = 0 + + begin + Thread.critical = true + + begin + tmpname = sprintf('%s/%s%d.%d', tmpdir, basename, $$, n) + lock = tmpname + '.lock' + n += 1 + end while @@cleanlist.include?(tmpname) or + File.exist?(lock) or File.exist?(tmpname) + + Dir.mkdir(lock) + rescue + failure += 1 + retry if failure < MAX_TRY + raise "cannot generate tempfile `%s'" % tmpname + ensure + Thread.critical = false + end + + @data = [tmpname] + @clean_proc = Tempfile.callback(@data) + ObjectSpace.define_finalizer(self, @clean_proc) + + @tmpfile = File.open(tmpname, File::RDWR|File::CREAT|File::EXCL, 0600) + @tmpname = tmpname + @@cleanlist << @tmpname + @data[1] = @tmpfile + @data[2] = @@cleanlist + + super(@tmpfile) + + # Now we have all the File/IO methods defined, you must not + # carelessly put bare puts(), etc. after this. + + Dir.rmdir(lock) + end + + # Opens or reopens the file with mode "r+". + def open + @tmpfile.close if @tmpfile + @tmpfile = File.open(@tmpname, 'r+') + @data[1] = @tmpfile + __setobj__(@tmpfile) + end + + def _close # :nodoc: + @tmpfile.close if @tmpfile + @data[1] = @tmpfile = nil + end + protected :_close + + # Closes the file. If the optional flag is true, unlinks the file + # after closing. + # + # If you don't explicitly unlink the temporary file, the removal + # will be delayed until the object is finalized. + def close(unlink_now=false) + if unlink_now + close! + else + _close + end + end + + # Closes and unlinks the file. + def close! + _close + @clean_proc.call + ObjectSpace.undefine_finalizer(self) + end + + # Unlinks the file. On UNIX-like systems, it is often a good idea + # to unlink a temporary file immediately after creating and opening + # it, because it leaves other programs zero chance to access the + # file. + def unlink + # keep this order for thread safeness + File.unlink(@tmpname) if File.exist?(@tmpname) + @@cleanlist.delete(@tmpname) if @@cleanlist + end + alias delete unlink + + if RUBY_VERSION > '1.8.0' + def __setobj__(obj) + @_dc_obj = obj + end + else + def __setobj__(obj) + @obj = obj + end + end + + # Returns the full path name of the temporary file. + def path + @tmpname + end + + # Returns the size of the temporary file. As a side effect, the IO + # buffer is flushed before determining the size. + def size + if @tmpfile + @tmpfile.flush + @tmpfile.stat.size + else + 0 + end + end + alias length size + + class << self + def callback(data) # :nodoc: + pid = $$ + lambda{ + if pid == $$ + path, tmpfile, cleanlist = *data + + print "removing ", path, "..." if $DEBUG + + tmpfile.close if tmpfile + + # keep this order for thread safeness + File.unlink(path) if File.exist?(path) + cleanlist.delete(path) if cleanlist + + print "done\n" if $DEBUG + end + } + end + + # If no block is given, this is a synonym for new(). + # + # If a block is given, it will be passed tempfile as an argument, + # and the tempfile will automatically be closed when the block + # terminates. In this case, open() returns nil. + def open(*args) + tempfile = new(*args) + + if block_given? + begin + yield(tempfile) + ensure + tempfile.close + end + + nil + else + tempfile + end + end + end +end + +end # module BugFix +if __FILE__ == $0 +# $DEBUG = true + f = Tempfile.new("foo") + f.print("foo\n") + f.close + f.open + p f.gets # => "foo\n" + f.close! +end diff --git a/vendor/plugins/rubyzip-0.9.1/lib/zip/zip.rb b/vendor/plugins/rubyzip-0.9.1/lib/zip/zip.rb new file mode 100755 index 00000000..19d90f51 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/lib/zip/zip.rb @@ -0,0 +1,1847 @@ +require 'delegate' +require 'singleton' +require 'tempfile' +require 'ftools' +require 'stringio' +require 'zlib' +require 'zip/stdrubyext' +require 'zip/ioextras' + +if Tempfile.superclass == SimpleDelegator + require 'zip/tempfile_bugfixed' + Tempfile = BugFix::Tempfile +end + +module Zlib #:nodoc:all + if ! const_defined? :MAX_WBITS + MAX_WBITS = Zlib::Deflate.MAX_WBITS + end +end + +module Zip + + VERSION = '0.9.1' + + RUBY_MINOR_VERSION = RUBY_VERSION.split(".")[1].to_i + + RUNNING_ON_WINDOWS = /mswin32|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM + + # Ruby 1.7.x compatibility + # In ruby 1.6.x and 1.8.0 reading from an empty stream returns + # an empty string the first time and then nil. + # not so in 1.7.x + EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST = RUBY_MINOR_VERSION != 7 + + # ZipInputStream is the basic class for reading zip entries in a + # zip file. It is possible to create a ZipInputStream object directly, + # passing the zip file name to the constructor, but more often than not + # the ZipInputStream will be obtained from a ZipFile (perhaps using the + # ZipFileSystem interface) object for a particular entry in the zip + # archive. + # + # A ZipInputStream inherits IOExtras::AbstractInputStream in order + # to provide an IO-like interface for reading from a single zip + # entry. Beyond methods for mimicking an IO-object it contains + # the method get_next_entry for iterating through the entries of + # an archive. get_next_entry returns a ZipEntry object that describes + # the zip entry the ZipInputStream is currently reading from. + # + # Example that creates a zip archive with ZipOutputStream and reads it + # back again with a ZipInputStream. + # + # require 'zip/zip' + # + # Zip::ZipOutputStream::open("my.zip") { + # |io| + # + # io.put_next_entry("first_entry.txt") + # io.write "Hello world!" + # + # io.put_next_entry("adir/first_entry.txt") + # io.write "Hello again!" + # } + # + # + # Zip::ZipInputStream::open("my.zip") { + # |io| + # + # while (entry = io.get_next_entry) + # puts "Contents of #{entry.name}: '#{io.read}'" + # end + # } + # + # java.util.zip.ZipInputStream is the original inspiration for this + # class. + + class ZipInputStream + include IOExtras::AbstractInputStream + + # Opens the indicated zip file. An exception is thrown + # if the specified offset in the specified filename is + # not a local zip entry header. + def initialize(filename, offset = 0) + super() + @archiveIO = File.open(filename, "rb") + @archiveIO.seek(offset, IO::SEEK_SET) + @decompressor = NullDecompressor.instance + @currentEntry = nil + end + + def close + @archiveIO.close + end + + # Same as #initialize but if a block is passed the opened + # stream is passed to the block and closed when the block + # returns. + def ZipInputStream.open(filename) + return new(filename) unless block_given? + + zio = new(filename) + yield zio + ensure + zio.close if zio + end + + # Returns a ZipEntry object. It is necessary to call this + # method on a newly created ZipInputStream before reading from + # the first entry in the archive. Returns nil when there are + # no more entries. + + def get_next_entry + @archiveIO.seek(@currentEntry.next_header_offset, + IO::SEEK_SET) if @currentEntry + open_entry + end + + # Rewinds the stream to the beginning of the current entry + def rewind + return if @currentEntry.nil? + @lineno = 0 + @archiveIO.seek(@currentEntry.localHeaderOffset, + IO::SEEK_SET) + open_entry + end + + # Modeled after IO.sysread + def sysread(numberOfBytes = nil, buf = nil) + @decompressor.sysread(numberOfBytes, buf) + end + + def eof + @outputBuffer.empty? && @decompressor.eof + end + alias :eof? :eof + + protected + + def open_entry + @currentEntry = ZipEntry.read_local_entry(@archiveIO) + if (@currentEntry == nil) + @decompressor = NullDecompressor.instance + elsif @currentEntry.compression_method == ZipEntry::STORED + @decompressor = PassThruDecompressor.new(@archiveIO, + @currentEntry.size) + elsif @currentEntry.compression_method == ZipEntry::DEFLATED + @decompressor = Inflater.new(@archiveIO) + else + raise ZipCompressionMethodError, + "Unsupported compression method #{@currentEntry.compression_method}" + end + flush + return @currentEntry + end + + def produce_input + @decompressor.produce_input + end + + def input_finished? + @decompressor.input_finished? + end + end + + + + class Decompressor #:nodoc:all + CHUNK_SIZE=32768 + def initialize(inputStream) + super() + @inputStream=inputStream + end + end + + class Inflater < Decompressor #:nodoc:all + def initialize(inputStream) + super + @zlibInflater = Zlib::Inflate.new(-Zlib::MAX_WBITS) + @outputBuffer="" + @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST + end + + def sysread(numberOfBytes = nil, buf = nil) + readEverything = (numberOfBytes == nil) + while (readEverything || @outputBuffer.length < numberOfBytes) + break if internal_input_finished? + @outputBuffer << internal_produce_input(buf) + end + return value_when_finished if @outputBuffer.length==0 && input_finished? + endIndex= numberOfBytes==nil ? @outputBuffer.length : numberOfBytes + return @outputBuffer.slice!(0...endIndex) + end + + def produce_input + if (@outputBuffer.empty?) + return internal_produce_input + else + return @outputBuffer.slice!(0...(@outputBuffer.length)) + end + end + + # to be used with produce_input, not read (as read may still have more data cached) + # is data cached anywhere other than @outputBuffer? the comment above may be wrong + def input_finished? + @outputBuffer.empty? && internal_input_finished? + end + alias :eof :input_finished? + alias :eof? :input_finished? + + private + + def internal_produce_input(buf = nil) + retried = 0 + begin + @zlibInflater.inflate(@inputStream.read(Decompressor::CHUNK_SIZE, buf)) + rescue Zlib::BufError + raise if (retried >= 5) # how many times should we retry? + retried += 1 + retry + end + end + + def internal_input_finished? + @zlibInflater.finished? + end + + # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ? + def value_when_finished # mimic behaviour of ruby File object. + return nil if @hasReturnedEmptyString + @hasReturnedEmptyString=true + return "" + end + end + + class PassThruDecompressor < Decompressor #:nodoc:all + def initialize(inputStream, charsToRead) + super inputStream + @charsToRead = charsToRead + @readSoFar = 0 + @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST + end + + # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ? + def sysread(numberOfBytes = nil, buf = nil) + if input_finished? + hasReturnedEmptyStringVal=@hasReturnedEmptyString + @hasReturnedEmptyString=true + return "" unless hasReturnedEmptyStringVal + return nil + end + + if (numberOfBytes == nil || @readSoFar+numberOfBytes > @charsToRead) + numberOfBytes = @charsToRead-@readSoFar + end + @readSoFar += numberOfBytes + @inputStream.read(numberOfBytes, buf) + end + + def produce_input + sysread(Decompressor::CHUNK_SIZE) + end + + def input_finished? + (@readSoFar >= @charsToRead) + end + alias :eof :input_finished? + alias :eof? :input_finished? + end + + class NullDecompressor #:nodoc:all + include Singleton + def sysread(numberOfBytes = nil, buf = nil) + nil + end + + def produce_input + nil + end + + def input_finished? + true + end + + def eof + true + end + alias :eof? :eof + end + + class NullInputStream < NullDecompressor #:nodoc:all + include IOExtras::AbstractInputStream + end + + class ZipEntry + STORED = 0 + DEFLATED = 8 + + FSTYPE_FAT = 0 + FSTYPE_AMIGA = 1 + FSTYPE_VMS = 2 + FSTYPE_UNIX = 3 + FSTYPE_VM_CMS = 4 + FSTYPE_ATARI = 5 + FSTYPE_HPFS = 6 + FSTYPE_MAC = 7 + FSTYPE_Z_SYSTEM = 8 + FSTYPE_CPM = 9 + FSTYPE_TOPS20 = 10 + FSTYPE_NTFS = 11 + FSTYPE_QDOS = 12 + FSTYPE_ACORN = 13 + FSTYPE_VFAT = 14 + FSTYPE_MVS = 15 + FSTYPE_BEOS = 16 + FSTYPE_TANDEM = 17 + FSTYPE_THEOS = 18 + FSTYPE_MAC_OSX = 19 + FSTYPE_ATHEOS = 30 + + FSTYPES = { + FSTYPE_FAT => 'FAT'.freeze, + FSTYPE_AMIGA => 'Amiga'.freeze, + FSTYPE_VMS => 'VMS (Vax or Alpha AXP)'.freeze, + FSTYPE_UNIX => 'Unix'.freeze, + FSTYPE_VM_CMS => 'VM/CMS'.freeze, + FSTYPE_ATARI => 'Atari ST'.freeze, + FSTYPE_HPFS => 'OS/2 or NT HPFS'.freeze, + FSTYPE_MAC => 'Macintosh'.freeze, + FSTYPE_Z_SYSTEM => 'Z-System'.freeze, + FSTYPE_CPM => 'CP/M'.freeze, + FSTYPE_TOPS20 => 'TOPS-20'.freeze, + FSTYPE_NTFS => 'NTFS'.freeze, + FSTYPE_QDOS => 'SMS/QDOS'.freeze, + FSTYPE_ACORN => 'Acorn RISC OS'.freeze, + FSTYPE_VFAT => 'Win32 VFAT'.freeze, + FSTYPE_MVS => 'MVS'.freeze, + FSTYPE_BEOS => 'BeOS'.freeze, + FSTYPE_TANDEM => 'Tandem NSK'.freeze, + FSTYPE_THEOS => 'Theos'.freeze, + FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)'.freeze, + FSTYPE_ATHEOS => 'AtheOS'.freeze, + }.freeze + + attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method, + :name, :size, :localHeaderOffset, :zipfile, :fstype, :externalFileAttributes, :gp_flags, :header_signature + + attr_accessor :follow_symlinks + attr_accessor :restore_times, :restore_permissions, :restore_ownership + attr_accessor :unix_uid, :unix_gid, :unix_perms + + attr_reader :ftype, :filepath # :nodoc: + + def initialize(zipfile = "", name = "", comment = "", extra = "", + compressed_size = 0, crc = 0, + compression_method = ZipEntry::DEFLATED, size = 0, + time = Time.now) + super() + if name.starts_with("/") + raise ZipEntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /" + end + @localHeaderOffset = 0 + @internalFileAttributes = 1 + @externalFileAttributes = 0 + @version = 52 # this library's version + @ftype = nil # unspecified or unknown + @filepath = nil + if Zip::RUNNING_ON_WINDOWS + @fstype = FSTYPE_FAT + else + @fstype = FSTYPE_UNIX + end + @zipfile, @comment, @compressed_size, @crc, @extra, @compression_method, + @name, @size = zipfile, comment, compressed_size, crc, + extra, compression_method, name, size + @time = time + + @follow_symlinks = false + + @restore_times = true + @restore_permissions = false + @restore_ownership = false + +# BUG: need an extra field to support uid/gid's + @unix_uid = nil + @unix_gid = nil + @unix_perms = nil +# @posix_acl = nil +# @ntfs_acl = nil + + if name_is_directory? + @ftype = :directory + else + @ftype = :file + end + + unless ZipExtraField === @extra + @extra = ZipExtraField.new(@extra.to_s) + end + end + + def time + if @extra["UniversalTime"] + @extra["UniversalTime"].mtime + else + # Atandard time field in central directory has local time + # under archive creator. Then, we can't get timezone. + @time + end + end + alias :mtime :time + + def time=(aTime) + unless @extra.member?("UniversalTime") + @extra.create("UniversalTime") + end + @extra["UniversalTime"].mtime = aTime + @time = aTime + end + + # Returns +true+ if the entry is a directory. + def directory? + raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype + @ftype == :directory + end + alias :is_directory :directory? + + # Returns +true+ if the entry is a file. + def file? + raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype + @ftype == :file + end + + # Returns +true+ if the entry is a symlink. + def symlink? + raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype + @ftype == :link + end + + def name_is_directory? #:nodoc:all + (%r{\/$} =~ @name) != nil + end + + def local_entry_offset #:nodoc:all + localHeaderOffset + local_header_size + end + + def local_header_size #:nodoc:all + LOCAL_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + (@extra ? @extra.local_size : 0) + end + + def cdir_header_size #:nodoc:all + CDIR_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + + (@extra ? @extra.c_dir_size : 0) + (@comment ? @comment.size : 0) + end + + def next_header_offset #:nodoc:all + local_entry_offset + self.compressed_size + end + + # Extracts entry to file destPath (defaults to @name). + def extract(destPath = @name, &onExistsProc) + onExistsProc ||= proc { false } + + if directory? + create_directory(destPath, &onExistsProc) + elsif file? + write_file(destPath, &onExistsProc) + elsif symlink? + create_symlink(destPath, &onExistsProc) + else + raise RuntimeError, "unknown file type #{self.inspect}" + end + + self + end + + def to_s + @name + end + + protected + + def ZipEntry.read_zip_short(io) # :nodoc: + io.read(2).unpack('v')[0] + end + + def ZipEntry.read_zip_long(io) # :nodoc: + io.read(4).unpack('V')[0] + end + public + + LOCAL_ENTRY_SIGNATURE = 0x04034b50 + LOCAL_ENTRY_STATIC_HEADER_LENGTH = 30 + LOCAL_ENTRY_TRAILING_DESCRIPTOR_LENGTH = 4+4+4 + + def read_local_entry(io) #:nodoc:all + @localHeaderOffset = io.tell + staticSizedFieldsBuf = io.read(LOCAL_ENTRY_STATIC_HEADER_LENGTH) + unless (staticSizedFieldsBuf.size==LOCAL_ENTRY_STATIC_HEADER_LENGTH) + raise ZipError, "Premature end of file. Not enough data for zip entry local header" + end + + @header_signature , + @version , + @fstype , + @gp_flags , + @compression_method, + lastModTime , + lastModDate , + @crc , + @compressed_size , + @size , + nameLength , + extraLength = staticSizedFieldsBuf.unpack('VCCvvvvVVVvv') + + unless (@header_signature == LOCAL_ENTRY_SIGNATURE) + raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'" + end + set_time(lastModDate, lastModTime) + + @name = io.read(nameLength) + extra = io.read(extraLength) + + if (extra && extra.length != extraLength) + raise ZipError, "Truncated local zip entry header" + else + if ZipExtraField === @extra + @extra.merge(extra) + else + @extra = ZipExtraField.new(extra) + end + end + end + + def ZipEntry.read_local_entry(io) + entry = new(io.path) + entry.read_local_entry(io) + return entry + rescue ZipError + return nil + end + + def write_local_entry(io) #:nodoc:all + @localHeaderOffset = io.tell + + io << + [LOCAL_ENTRY_SIGNATURE , + 0 , + 0 , # @gp_flags , + @compression_method , + @time.to_binary_dos_time , # @lastModTime , + @time.to_binary_dos_date , # @lastModDate , + @crc , + @compressed_size , + @size , + @name ? @name.length : 0, + @extra? @extra.local_length : 0 ].pack('VvvvvvVVVvv') + io << @name + io << (@extra ? @extra.to_local_bin : "") + end + + CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50 + CDIR_ENTRY_STATIC_HEADER_LENGTH = 46 + + def read_c_dir_entry(io) #:nodoc:all + staticSizedFieldsBuf = io.read(CDIR_ENTRY_STATIC_HEADER_LENGTH) + unless (staticSizedFieldsBuf.size == CDIR_ENTRY_STATIC_HEADER_LENGTH) + raise ZipError, "Premature end of file. Not enough data for zip cdir entry header" + end + + @header_signature , + @version , # version of encoding software + @fstype , # filesystem type + @versionNeededToExtract, + @gp_flags , + @compression_method , + lastModTime , + lastModDate , + @crc , + @compressed_size , + @size , + nameLength , + extraLength , + commentLength , + diskNumberStart , + @internalFileAttributes, + @externalFileAttributes, + @localHeaderOffset , + @name , + @extra , + @comment = staticSizedFieldsBuf.unpack('VCCvvvvvVVVvvvvvVV') + + unless (@header_signature == CENTRAL_DIRECTORY_ENTRY_SIGNATURE) + raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'" + end + set_time(lastModDate, lastModTime) + + @name = io.read(nameLength) + if ZipExtraField === @extra + @extra.merge(io.read(extraLength)) + else + @extra = ZipExtraField.new(io.read(extraLength)) + end + @comment = io.read(commentLength) + unless (@comment && @comment.length == commentLength) + raise ZipError, "Truncated cdir zip entry header" + end + + case @fstype + when FSTYPE_UNIX + @unix_perms = (@externalFileAttributes >> 16) & 07777 + + case (@externalFileAttributes >> 28) + when 04 + @ftype = :directory + when 010 + @ftype = :file + when 012 + @ftype = :link + else + raise ZipInternalError, "unknown file type #{'0%o' % (@externalFileAttributes >> 28)}" + end + else + if name_is_directory? + @ftype = :directory + else + @ftype = :file + end + end + end + + def ZipEntry.read_c_dir_entry(io) #:nodoc:all + entry = new(io.path) + entry.read_c_dir_entry(io) + return entry + rescue ZipError + return nil + end + + def file_stat(path) # :nodoc: + if @follow_symlinks + return File::stat(path) + else + return File::lstat(path) + end + end + + def get_extra_attributes_from_path(path) # :nodoc: + unless Zip::RUNNING_ON_WINDOWS + stat = file_stat(path) + @unix_uid = stat.uid + @unix_gid = stat.gid + @unix_perms = stat.mode & 07777 + end + end + + def set_extra_attributes_on_path(destPath) # :nodoc: + return unless (file? or directory?) + + case @fstype + when FSTYPE_UNIX + # BUG: does not update timestamps into account + # ignore setuid/setgid bits by default. honor if @restore_ownership + unix_perms_mask = 01777 + unix_perms_mask = 07777 if (@restore_ownership) + File::chmod(@unix_perms & unix_perms_mask, destPath) if (@restore_permissions && @unix_perms) + File::chown(@unix_uid, @unix_gid, destPath) if (@restore_ownership && @unix_uid && @unix_gid && Process::egid == 0) + # File::utimes() + end + end + + def write_c_dir_entry(io) #:nodoc:all + case @fstype + when FSTYPE_UNIX + ft = nil + case @ftype + when :file + ft = 010 + @unix_perms ||= 0644 + when :directory + ft = 004 + @unix_perms ||= 0755 + when :symlink + ft = 012 + @unix_perms ||= 0755 + else + raise ZipInternalError, "unknown file type #{self.inspect}" + end + + @externalFileAttributes = (ft << 12 | (@unix_perms & 07777)) << 16 + end + + io << + [CENTRAL_DIRECTORY_ENTRY_SIGNATURE, + @version , # version of encoding software + @fstype , # filesystem type + 0 , # @versionNeededToExtract , + 0 , # @gp_flags , + @compression_method , + @time.to_binary_dos_time , # @lastModTime , + @time.to_binary_dos_date , # @lastModDate , + @crc , + @compressed_size , + @size , + @name ? @name.length : 0 , + @extra ? @extra.c_dir_length : 0 , + @comment ? comment.length : 0 , + 0 , # disk number start + @internalFileAttributes , # file type (binary=0, text=1) + @externalFileAttributes , # native filesystem attributes + @localHeaderOffset , + @name , + @extra , + @comment ].pack('VCCvvvvvVVVvvvvvVV') + + io << @name + io << (@extra ? @extra.to_c_dir_bin : "") + io << @comment + end + + def == (other) + return false unless other.class == self.class + # Compares contents of local entry and exposed fields + (@compression_method == other.compression_method && + @crc == other.crc && + @compressed_size == other.compressed_size && + @size == other.size && + @name == other.name && + @extra == other.extra && + @filepath == other.filepath && + self.time.dos_equals(other.time)) + end + + def <=> (other) + return to_s <=> other.to_s + end + + # Returns an IO like object for the given ZipEntry. + # Warning: may behave weird with symlinks. + def get_input_stream(&aProc) + if @ftype == :directory + return yield(NullInputStream.instance) if block_given? + return NullInputStream.instance + elsif @filepath + case @ftype + when :file + return File.open(@filepath, "rb", &aProc) + + when :symlink + linkpath = File::readlink(@filepath) + stringio = StringIO.new(linkpath) + return yield(stringio) if block_given? + return stringio + else + raise "unknown @ftype #{@ftype}" + end + else + zis = ZipInputStream.new(@zipfile, localHeaderOffset) + zis.get_next_entry + if block_given? + begin + return yield(zis) + ensure + zis.close + end + else + return zis + end + end + end + + def gather_fileinfo_from_srcpath(srcPath) # :nodoc: + stat = file_stat(srcPath) + case stat.ftype + when 'file' + if name_is_directory? + raise ArgumentError, + "entry name '#{newEntry}' indicates directory entry, but "+ + "'#{srcPath}' is not a directory" + end + @ftype = :file + when 'directory' + if ! name_is_directory? + @name += "/" + end + @ftype = :directory + when 'link' + if name_is_directory? + raise ArgumentError, + "entry name '#{newEntry}' indicates directory entry, but "+ + "'#{srcPath}' is not a directory" + end + @ftype = :symlink + else + raise RuntimeError, "unknown file type: #{srcPath.inspect} #{stat.inspect}" + end + + @filepath = srcPath + get_extra_attributes_from_path(@filepath) + end + + def write_to_zip_output_stream(aZipOutputStream) #:nodoc:all + if @ftype == :directory + aZipOutputStream.put_next_entry(self) + elsif @filepath + aZipOutputStream.put_next_entry(self) + get_input_stream { |is| IOExtras.copy_stream(aZipOutputStream, is) } + else + aZipOutputStream.copy_raw_entry(self) + end + end + + def parent_as_string + entry_name = name.chomp("/") + slash_index = entry_name.rindex("/") + slash_index ? entry_name.slice(0, slash_index+1) : nil + end + + def get_raw_input_stream(&aProc) + File.open(@zipfile, "rb", &aProc) + end + + private + + def set_time(binaryDosDate, binaryDosTime) + @time = Time.parse_binary_dos_format(binaryDosDate, binaryDosTime) + rescue ArgumentError + puts "Invalid date/time in zip entry" + end + + def write_file(destPath, continueOnExistsProc = proc { false }) + if File.exists?(destPath) && ! yield(self, destPath) + raise ZipDestinationFileExistsError, + "Destination '#{destPath}' already exists" + end + File.open(destPath, "wb") do |os| + get_input_stream do |is| + set_extra_attributes_on_path(destPath) + + buf = '' + while buf = is.sysread(Decompressor::CHUNK_SIZE, buf) + os << buf + end + end + end + end + + def create_directory(destPath) + if File.directory? destPath + return + elsif File.exists? destPath + if block_given? && yield(self, destPath) + File.rm_f destPath + else + raise ZipDestinationFileExistsError, + "Cannot create directory '#{destPath}'. "+ + "A file already exists with that name" + end + end + Dir.mkdir destPath + set_extra_attributes_on_path(destPath) + end + +# BUG: create_symlink() does not use &onExistsProc + def create_symlink(destPath) + stat = nil + begin + stat = File::lstat(destPath) + rescue Errno::ENOENT + end + + io = get_input_stream + linkto = io.read + + if stat + if stat.symlink? + if File::readlink(destPath) == linkto + return + else + raise ZipDestinationFileExistsError, + "Cannot create symlink '#{destPath}'. "+ + "A symlink already exists with that name" + end + else + raise ZipDestinationFileExistsError, + "Cannot create symlink '#{destPath}'. "+ + "A file already exists with that name" + end + end + + File::symlink(linkto, destPath) + end + end + + + # ZipOutputStream is the basic class for writing zip files. It is + # possible to create a ZipOutputStream object directly, passing + # the zip file name to the constructor, but more often than not + # the ZipOutputStream will be obtained from a ZipFile (perhaps using the + # ZipFileSystem interface) object for a particular entry in the zip + # archive. + # + # A ZipOutputStream inherits IOExtras::AbstractOutputStream in order + # to provide an IO-like interface for writing to a single zip + # entry. Beyond methods for mimicking an IO-object it contains + # the method put_next_entry that closes the current entry + # and creates a new. + # + # Please refer to ZipInputStream for example code. + # + # java.util.zip.ZipOutputStream is the original inspiration for this + # class. + + class ZipOutputStream + include IOExtras::AbstractOutputStream + + attr_accessor :comment + + # Opens the indicated zip file. If a file with that name already + # exists it will be overwritten. + def initialize(fileName) + super() + @fileName = fileName + @outputStream = File.new(@fileName, "wb") + @entrySet = ZipEntrySet.new + @compressor = NullCompressor.instance + @closed = false + @currentEntry = nil + @comment = nil + end + + # Same as #initialize but if a block is passed the opened + # stream is passed to the block and closed when the block + # returns. + def ZipOutputStream.open(fileName) + return new(fileName) unless block_given? + zos = new(fileName) + yield zos + ensure + zos.close if zos + end + + # Closes the stream and writes the central directory to the zip file + def close + return if @closed + finalize_current_entry + update_local_headers + write_central_directory + @outputStream.close + @closed = true + end + + # Closes the current entry and opens a new for writing. + # +entry+ can be a ZipEntry object or a string. + def put_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION) + raise ZipError, "zip stream is closed" if @closed + newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@fileName, entry.to_s) + init_next_entry(newEntry, level) + @currentEntry=newEntry + end + + def copy_raw_entry(entry) + entry = entry.dup + raise ZipError, "zip stream is closed" if @closed + raise ZipError, "entry is not a ZipEntry" if !entry.kind_of?(ZipEntry) + finalize_current_entry + @entrySet << entry + src_pos = entry.local_entry_offset + entry.write_local_entry(@outputStream) + @compressor = NullCompressor.instance + @outputStream << entry.get_raw_input_stream { + |is| + is.seek(src_pos, IO::SEEK_SET) + is.read(entry.compressed_size) + } + @compressor = NullCompressor.instance + @currentEntry = nil + end + + private + def finalize_current_entry + return unless @currentEntry + finish + @currentEntry.compressed_size = @outputStream.tell - @currentEntry.localHeaderOffset - + @currentEntry.local_header_size + @currentEntry.size = @compressor.size + @currentEntry.crc = @compressor.crc + @currentEntry = nil + @compressor = NullCompressor.instance + end + + def init_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION) + finalize_current_entry + @entrySet << entry + entry.write_local_entry(@outputStream) + @compressor = get_compressor(entry, level) + end + + def get_compressor(entry, level) + case entry.compression_method + when ZipEntry::DEFLATED then Deflater.new(@outputStream, level) + when ZipEntry::STORED then PassThruCompressor.new(@outputStream) + else raise ZipCompressionMethodError, + "Invalid compression method: '#{entry.compression_method}'" + end + end + + def update_local_headers + pos = @outputStream.tell + @entrySet.each { + |entry| + @outputStream.pos = entry.localHeaderOffset + entry.write_local_entry(@outputStream) + } + @outputStream.pos = pos + end + + def write_central_directory + cdir = ZipCentralDirectory.new(@entrySet, @comment) + cdir.write_to_stream(@outputStream) + end + + protected + + def finish + @compressor.finish + end + + public + # Modeled after IO.<< + def << (data) + @compressor << data + end + end + + + class Compressor #:nodoc:all + def finish + end + end + + class PassThruCompressor < Compressor #:nodoc:all + def initialize(outputStream) + super() + @outputStream = outputStream + @crc = Zlib::crc32 + @size = 0 + end + + def << (data) + val = data.to_s + @crc = Zlib::crc32(val, @crc) + @size += val.size + @outputStream << val + end + + attr_reader :size, :crc + end + + class NullCompressor < Compressor #:nodoc:all + include Singleton + + def << (data) + raise IOError, "closed stream" + end + + attr_reader :size, :compressed_size + end + + class Deflater < Compressor #:nodoc:all + def initialize(outputStream, level = Zlib::DEFAULT_COMPRESSION) + super() + @outputStream = outputStream + @zlibDeflater = Zlib::Deflate.new(level, -Zlib::MAX_WBITS) + @size = 0 + @crc = Zlib::crc32 + end + + def << (data) + val = data.to_s + @crc = Zlib::crc32(val, @crc) + @size += val.size + @outputStream << @zlibDeflater.deflate(data) + end + + def finish + until @zlibDeflater.finished? + @outputStream << @zlibDeflater.finish + end + end + + attr_reader :size, :crc + end + + + class ZipEntrySet #:nodoc:all + include Enumerable + + def initialize(anEnumerable = []) + super() + @entrySet = {} + anEnumerable.each { |o| push(o) } + end + + def include?(entry) + @entrySet.include?(entry.to_s) + end + + def <<(entry) + @entrySet[entry.to_s] = entry + end + alias :push :<< + + def size + @entrySet.size + end + alias :length :size + + def delete(entry) + @entrySet.delete(entry.to_s) ? entry : nil + end + + def each(&aProc) + @entrySet.values.each(&aProc) + end + + def entries + @entrySet.values + end + + # deep clone + def dup + newZipEntrySet = ZipEntrySet.new(@entrySet.values.map { |e| e.dup }) + end + + def == (other) + return false unless other.kind_of?(ZipEntrySet) + return @entrySet == other.entrySet + end + + def parent(entry) + @entrySet[entry.parent_as_string] + end + + def glob(pattern, flags = File::FNM_PATHNAME|File::FNM_DOTMATCH) + entries.select { + |entry| + File.fnmatch(pattern, entry.name.chomp('/'), flags) + } + end + +#TODO attr_accessor :auto_create_directories + protected + attr_accessor :entrySet + end + + + class ZipCentralDirectory + include Enumerable + + END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50 + MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE = 65536 + 18 + STATIC_EOCD_SIZE = 22 + + attr_reader :comment + + # Returns an Enumerable containing the entries. + def entries + @entrySet.entries + end + + def initialize(entries = ZipEntrySet.new, comment = "") #:nodoc: + super() + @entrySet = entries.kind_of?(ZipEntrySet) ? entries : ZipEntrySet.new(entries) + @comment = comment + end + + def write_to_stream(io) #:nodoc: + offset = io.tell + @entrySet.each { |entry| entry.write_c_dir_entry(io) } + write_e_o_c_d(io, offset) + end + + def write_e_o_c_d(io, offset) #:nodoc: + io << + [END_OF_CENTRAL_DIRECTORY_SIGNATURE, + 0 , # @numberOfThisDisk + 0 , # @numberOfDiskWithStartOfCDir + @entrySet? @entrySet.size : 0 , + @entrySet? @entrySet.size : 0 , + cdir_size , + offset , + @comment ? @comment.length : 0 ].pack('VvvvvVVv') + io << @comment + end + private :write_e_o_c_d + + def cdir_size #:nodoc: + # does not include eocd + @entrySet.inject(0) { |value, entry| entry.cdir_header_size + value } + end + private :cdir_size + + def read_e_o_c_d(io) #:nodoc: + buf = get_e_o_c_d(io) + @numberOfThisDisk = ZipEntry::read_zip_short(buf) + @numberOfDiskWithStartOfCDir = ZipEntry::read_zip_short(buf) + @totalNumberOfEntriesInCDirOnThisDisk = ZipEntry::read_zip_short(buf) + @size = ZipEntry::read_zip_short(buf) + @sizeInBytes = ZipEntry::read_zip_long(buf) + @cdirOffset = ZipEntry::read_zip_long(buf) + commentLength = ZipEntry::read_zip_short(buf) + @comment = buf.read(commentLength) + raise ZipError, "Zip consistency problem while reading eocd structure" unless buf.size == 0 + end + + def read_central_directory_entries(io) #:nodoc: + begin + io.seek(@cdirOffset, IO::SEEK_SET) + rescue Errno::EINVAL + raise ZipError, "Zip consistency problem while reading central directory entry" + end + @entrySet = ZipEntrySet.new + @size.times { + @entrySet << ZipEntry.read_c_dir_entry(io) + } + end + + def read_from_stream(io) #:nodoc: + read_e_o_c_d(io) + read_central_directory_entries(io) + end + + def get_e_o_c_d(io) #:nodoc: + begin + io.seek(-MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE, IO::SEEK_END) + rescue Errno::EINVAL + io.seek(0, IO::SEEK_SET) + rescue Errno::EFBIG # FreeBSD 4.9 raise Errno::EFBIG instead of Errno::EINVAL + io.seek(0, IO::SEEK_SET) + end + + # 'buf = io.read' substituted with lump of code to work around FreeBSD 4.5 issue + retried = false + buf = nil + begin + buf = io.read + rescue Errno::EFBIG # FreeBSD 4.5 may raise Errno::EFBIG + raise if (retried) + retried = true + + io.seek(0, IO::SEEK_SET) + retry + end + + sigIndex = buf.rindex([END_OF_CENTRAL_DIRECTORY_SIGNATURE].pack('V')) + raise ZipError, "Zip end of central directory signature not found" unless sigIndex + buf=buf.slice!((sigIndex+4)...(buf.size)) + def buf.read(count) + slice!(0, count) + end + return buf + end + + # For iterating over the entries. + def each(&proc) + @entrySet.each(&proc) + end + + # Returns the number of entries in the central directory (and + # consequently in the zip archive). + def size + @entrySet.size + end + + def ZipCentralDirectory.read_from_stream(io) #:nodoc: + cdir = new + cdir.read_from_stream(io) + return cdir + rescue ZipError + return nil + end + + def == (other) #:nodoc: + return false unless other.kind_of?(ZipCentralDirectory) + @entrySet.entries.sort == other.entries.sort && comment == other.comment + end + end + + + class ZipError < StandardError ; end + + class ZipEntryExistsError < ZipError; end + class ZipDestinationFileExistsError < ZipError; end + class ZipCompressionMethodError < ZipError; end + class ZipEntryNameError < ZipError; end + class ZipInternalError < ZipError; end + + # ZipFile is modeled after java.util.zip.ZipFile from the Java SDK. + # The most important methods are those inherited from + # ZipCentralDirectory for accessing information about the entries in + # the archive and methods such as get_input_stream and + # get_output_stream for reading from and writing entries to the + # archive. The class includes a few convenience methods such as + # #extract for extracting entries to the filesystem, and #remove, + # #replace, #rename and #mkdir for making simple modifications to + # the archive. + # + # Modifications to a zip archive are not committed until #commit or + # #close is called. The method #open accepts a block following + # the pattern from File.open offering a simple way to + # automatically close the archive when the block returns. + # + # The following example opens zip archive my.zip + # (creating it if it doesn't exist) and adds an entry + # first.txt and a directory entry a_dir + # to it. + # + # require 'zip/zip' + # + # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) { + # |zipfile| + # zipfile.get_output_stream("first.txt") { |f| f.puts "Hello from ZipFile" } + # zipfile.mkdir("a_dir") + # } + # + # The next example reopens my.zip writes the contents of + # first.txt to standard out and deletes the entry from + # the archive. + # + # require 'zip/zip' + # + # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) { + # |zipfile| + # puts zipfile.read("first.txt") + # zipfile.remove("first.txt") + # } + # + # ZipFileSystem offers an alternative API that emulates ruby's + # interface for accessing the filesystem, ie. the File and Dir classes. + + class ZipFile < ZipCentralDirectory + + CREATE = 1 + + attr_reader :name + + # default -> false + attr_accessor :restore_ownership + # default -> false + attr_accessor :restore_permissions + # default -> true + attr_accessor :restore_times + + # Opens a zip archive. Pass true as the second parameter to create + # a new archive if it doesn't exist already. + def initialize(fileName, create = nil) + super() + @name = fileName + @comment = "" + if (File.exists?(fileName)) + File.open(name, "rb") { |f| read_from_stream(f) } + elsif (create) + @entrySet = ZipEntrySet.new + else + raise ZipError, "File #{fileName} not found" + end + @create = create + @storedEntries = @entrySet.dup + + @restore_ownership = false + @restore_permissions = false + @restore_times = true + end + + # Same as #new. If a block is passed the ZipFile object is passed + # to the block and is automatically closed afterwards just as with + # ruby's builtin File.open method. + def ZipFile.open(fileName, create = nil) + zf = ZipFile.new(fileName, create) + if block_given? + begin + yield zf + ensure + zf.close + end + else + zf + end + end + + # Returns the zip files comment, if it has one + attr_accessor :comment + + # Iterates over the contents of the ZipFile. This is more efficient + # than using a ZipInputStream since this methods simply iterates + # through the entries in the central directory structure in the archive + # whereas ZipInputStream jumps through the entire archive accessing the + # local entry headers (which contain the same information as the + # central directory). + def ZipFile.foreach(aZipFileName, &block) + ZipFile.open(aZipFileName) { + |zipFile| + zipFile.each(&block) + } + end + + # Returns an input stream to the specified entry. If a block is passed + # the stream object is passed to the block and the stream is automatically + # closed afterwards just as with ruby's builtin File.open method. + def get_input_stream(entry, &aProc) + get_entry(entry).get_input_stream(&aProc) + end + + # Returns an output stream to the specified entry. If a block is passed + # the stream object is passed to the block and the stream is automatically + # closed afterwards just as with ruby's builtin File.open method. + def get_output_stream(entry, &aProc) + newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s) + if newEntry.directory? + raise ArgumentError, + "cannot open stream to directory entry - '#{newEntry}'" + end + zipStreamableEntry = ZipStreamableStream.new(newEntry) + @entrySet << zipStreamableEntry + zipStreamableEntry.get_output_stream(&aProc) + end + + # Returns the name of the zip archive + def to_s + @name + end + + # Returns a string containing the contents of the specified entry + def read(entry) + get_input_stream(entry) { |is| is.read } + end + + # Convenience method for adding the contents of a file to the archive + def add(entry, srcPath, &continueOnExistsProc) + continueOnExistsProc ||= proc { false } + check_entry_exists(entry, continueOnExistsProc, "add") + newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s) + newEntry.gather_fileinfo_from_srcpath(srcPath) + @entrySet << newEntry + end + + # Removes the specified entry. + def remove(entry) + @entrySet.delete(get_entry(entry)) + end + + # Renames the specified entry. + def rename(entry, newName, &continueOnExistsProc) + foundEntry = get_entry(entry) + check_entry_exists(newName, continueOnExistsProc, "rename") + foundEntry.name=newName + end + + # Replaces the specified entry with the contents of srcPath (from + # the file system). + def replace(entry, srcPath) + check_file(srcPath) + add(remove(entry), srcPath) + end + + # Extracts entry to file destPath. + def extract(entry, destPath, &onExistsProc) + onExistsProc ||= proc { false } + foundEntry = get_entry(entry) + foundEntry.extract(destPath, &onExistsProc) + end + + # Commits changes that has been made since the previous commit to + # the zip archive. + def commit + return if ! commit_required? + on_success_replace(name) { + |tmpFile| + ZipOutputStream.open(tmpFile) { + |zos| + + @entrySet.each { |e| e.write_to_zip_output_stream(zos) } + zos.comment = comment + } + true + } + initialize(name) + end + + # Closes the zip file committing any changes that has been made. + def close + commit + end + + # Returns true if any changes has been made to this archive since + # the previous commit + def commit_required? + return @entrySet != @storedEntries || @create == ZipFile::CREATE + end + + # Searches for entry with the specified name. Returns nil if + # no entry is found. See also get_entry + def find_entry(entry) + @entrySet.detect { + |e| + e.name.sub(/\/$/, "") == entry.to_s.sub(/\/$/, "") + } + end + + # Searches for an entry just as find_entry, but throws Errno::ENOENT + # if no entry is found. + def get_entry(entry) + selectedEntry = find_entry(entry) + unless selectedEntry + raise Errno::ENOENT, entry + end + selectedEntry.restore_ownership = @restore_ownership + selectedEntry.restore_permissions = @restore_permissions + selectedEntry.restore_times = @restore_times + + return selectedEntry + end + + # Creates a directory + def mkdir(entryName, permissionInt = 0755) + if find_entry(entryName) + raise Errno::EEXIST, "File exists - #{entryName}" + end + @entrySet << ZipStreamableDirectory.new(@name, entryName.to_s.ensure_end("/"), nil, permissionInt) + end + + private + + def is_directory(newEntry, srcPath) + srcPathIsDirectory = File.directory?(srcPath) + if newEntry.is_directory && ! srcPathIsDirectory + raise ArgumentError, + "entry name '#{newEntry}' indicates directory entry, but "+ + "'#{srcPath}' is not a directory" + elsif ! newEntry.is_directory && srcPathIsDirectory + newEntry.name += "/" + end + return newEntry.is_directory && srcPathIsDirectory + end + + def check_entry_exists(entryName, continueOnExistsProc, procedureName) + continueOnExistsProc ||= proc { false } + if @entrySet.detect { |e| e.name == entryName } + if continueOnExistsProc.call + remove get_entry(entryName) + else + raise ZipEntryExistsError, + procedureName+" failed. Entry #{entryName} already exists" + end + end + end + + def check_file(path) + unless File.readable? path + raise Errno::ENOENT, path + end + end + + def on_success_replace(aFilename) + tmpfile = get_tempfile + tmpFilename = tmpfile.path + tmpfile.close + if yield tmpFilename + File.move(tmpFilename, name) + end + end + + def get_tempfile + tempFile = Tempfile.new(File.basename(name), File.dirname(name)) + tempFile.binmode + tempFile + end + + end + + class ZipStreamableDirectory < ZipEntry + def initialize(zipfile, entry, srcPath = nil, permissionInt = nil) + super(zipfile, entry) + + @ftype = :directory + entry.get_extra_attributes_from_path(srcPath) if (srcPath) + @unix_perms = permissionInt if (permissionInt) + end + end + + class ZipStreamableStream < DelegateClass(ZipEntry) #nodoc:all + def initialize(entry) + super(entry) + @tempFile = Tempfile.new(File.basename(name), File.dirname(zipfile)) + @tempFile.binmode + end + + def get_output_stream + if block_given? + begin + yield(@tempFile) + ensure + @tempFile.close + end + else + @tempFile + end + end + + def get_input_stream + if ! @tempFile.closed? + raise StandardError, "cannot open entry for reading while its open for writing - #{name}" + end + @tempFile.open # reopens tempfile from top + @tempFile.binmode + if block_given? + begin + yield(@tempFile) + ensure + @tempFile.close + end + else + @tempFile + end + end + + def write_to_zip_output_stream(aZipOutputStream) + aZipOutputStream.put_next_entry(self) + get_input_stream { |is| IOExtras.copy_stream(aZipOutputStream, is) } + end + end + + class ZipExtraField < Hash + ID_MAP = {} + + # Meta class for extra fields + class Generic + def self.register_map + if self.const_defined?(:HEADER_ID) + ID_MAP[self.const_get(:HEADER_ID)] = self + end + end + + def self.name + self.to_s.split("::")[-1] + end + + # return field [size, content] or false + def initial_parse(binstr) + if ! binstr + # If nil, start with empty. + return false + elsif binstr[0,2] != self.class.const_get(:HEADER_ID) + $stderr.puts "Warning: weired extra feild header ID. skip parsing" + return false + end + [binstr[2,2].unpack("v")[0], binstr[4..-1]] + end + + def ==(other) + self.class != other.class and return false + each { |k, v| + v != other[k] and return false + } + true + end + + def to_local_bin + s = pack_for_local + self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s + end + + def to_c_dir_bin + s = pack_for_c_dir + self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s + end + end + + # Info-ZIP Additional timestamp field + class UniversalTime < Generic + HEADER_ID = "UT" + register_map + + def initialize(binstr = nil) + @ctime = nil + @mtime = nil + @atime = nil + @flag = nil + binstr and merge(binstr) + end + attr_accessor :atime, :ctime, :mtime, :flag + + def merge(binstr) + binstr == "" and return + size, content = initial_parse(binstr) + size or return + @flag, mtime, atime, ctime = content.unpack("CVVV") + mtime and @mtime ||= Time.at(mtime) + atime and @atime ||= Time.at(atime) + ctime and @ctime ||= Time.at(ctime) + end + + def ==(other) + @mtime == other.mtime && + @atime == other.atime && + @ctime == other.ctime + end + + def pack_for_local + s = [@flag].pack("C") + @flag & 1 != 0 and s << [@mtime.to_i].pack("V") + @flag & 2 != 0 and s << [@atime.to_i].pack("V") + @flag & 4 != 0 and s << [@ctime.to_i].pack("V") + s + end + + def pack_for_c_dir + s = [@flag].pack("C") + @flag & 1 == 1 and s << [@mtime.to_i].pack("V") + s + end + end + + # Info-ZIP Extra for UNIX uid/gid + class IUnix < Generic + HEADER_ID = "Ux" + register_map + + def initialize(binstr = nil) + @uid = 0 + @gid = 0 + binstr and merge(binstr) + end + attr_accessor :uid, :gid + + def merge(binstr) + binstr == "" and return + size, content = initial_parse(binstr) + # size: 0 for central direcotry. 4 for local header + return if(! size || size == 0) + uid, gid = content.unpack("vv") + @uid ||= uid + @gid ||= gid + end + + def ==(other) + @uid == other.uid && + @gid == other.gid + end + + def pack_for_local + [@uid, @gid].pack("vv") + end + + def pack_for_c_dir + "" + end + end + + ## start main of ZipExtraField < Hash + def initialize(binstr = nil) + binstr and merge(binstr) + end + + def merge(binstr) + binstr == "" and return + i = 0 + while i < binstr.length + id = binstr[i,2] + len = binstr[i+2,2].to_s.unpack("v")[0] + if id && ID_MAP.member?(id) + field_name = ID_MAP[id].name + if self.member?(field_name) + self[field_name].mergea(binstr[i, len+4]) + else + field_obj = ID_MAP[id].new(binstr[i, len+4]) + self[field_name] = field_obj + end + elsif id + unless self["Unknown"] + s = "" + class << s + alias_method :to_c_dir_bin, :to_s + alias_method :to_local_bin, :to_s + end + self["Unknown"] = s + end + if ! len || len+4 > binstr[i..-1].length + self["Unknown"] << binstr[i..-1] + break; + end + self["Unknown"] << binstr[i, len+4] + end + i += len+4 + end + end + + def create(name) + field_class = nil + ID_MAP.each { |id, klass| + if klass.name == name + field_class = klass + break + end + } + if ! field_class + raise ZipError, "Unknown extra field '#{name}'" + end + self[name] = field_class.new() + end + + def to_local_bin + s = "" + each { |k, v| + s << v.to_local_bin + } + s + end + alias :to_s :to_local_bin + + def to_c_dir_bin + s = "" + each { |k, v| + s << v.to_c_dir_bin + } + s + end + + def c_dir_length + to_c_dir_bin.length + end + def local_length + to_local_bin.length + end + alias :c_dir_size :c_dir_length + alias :local_size :local_length + alias :length :local_length + alias :size :local_length + end # end ZipExtraField + +end # Zip namespace module + + + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/lib/zip/zipfilesystem.rb b/vendor/plugins/rubyzip-0.9.1/lib/zip/zipfilesystem.rb new file mode 100755 index 00000000..3fa3748c --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/lib/zip/zipfilesystem.rb @@ -0,0 +1,609 @@ +require 'zip/zip' + +module Zip + + # The ZipFileSystem API provides an API for accessing entries in + # a zip archive that is similar to ruby's builtin File and Dir + # classes. + # + # Requiring 'zip/zipfilesystem' includes this module in ZipFile + # making the methods in this module available on ZipFile objects. + # + # Using this API the following example creates a new zip file + # my.zip containing a normal entry with the name + # first.txt, a directory entry named mydir + # and finally another normal entry named second.txt + # + # require 'zip/zipfilesystem' + # + # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) { + # |zipfile| + # zipfile.file.open("first.txt", "w") { |f| f.puts "Hello world" } + # zipfile.dir.mkdir("mydir") + # zipfile.file.open("mydir/second.txt", "w") { |f| f.puts "Hello again" } + # } + # + # Reading is as easy as writing, as the following example shows. The + # example writes the contents of first.txt from zip archive + # my.zip to standard out. + # + # require 'zip/zipfilesystem' + # + # Zip::ZipFile.open("my.zip") { + # |zipfile| + # puts zipfile.file.read("first.txt") + # } + + module ZipFileSystem + + def initialize # :nodoc: + mappedZip = ZipFileNameMapper.new(self) + @zipFsDir = ZipFsDir.new(mappedZip) + @zipFsFile = ZipFsFile.new(mappedZip) + @zipFsDir.file = @zipFsFile + @zipFsFile.dir = @zipFsDir + end + + # Returns a ZipFsDir which is much like ruby's builtin Dir (class) + # object, except it works on the ZipFile on which this method is + # invoked + def dir + @zipFsDir + end + + # Returns a ZipFsFile which is much like ruby's builtin File (class) + # object, except it works on the ZipFile on which this method is + # invoked + def file + @zipFsFile + end + + # Instances of this class are normally accessed via the accessor + # ZipFile::file. An instance of ZipFsFile behaves like ruby's + # builtin File (class) object, except it works on ZipFile entries. + # + # The individual methods are not documented due to their + # similarity with the methods in File + class ZipFsFile + + attr_writer :dir +# protected :dir + + class ZipFsStat + def initialize(zipFsFile, entryName) + @zipFsFile = zipFsFile + @entryName = entryName + end + + def forward_invoke(msg) + @zipFsFile.send(msg, @entryName) + end + + def kind_of?(t) + super || t == ::File::Stat + end + + forward_message :forward_invoke, :file?, :directory?, :pipe?, :chardev? + forward_message :forward_invoke, :symlink?, :socket?, :blockdev? + forward_message :forward_invoke, :readable?, :readable_real? + forward_message :forward_invoke, :writable?, :writable_real? + forward_message :forward_invoke, :executable?, :executable_real? + forward_message :forward_invoke, :sticky?, :owned?, :grpowned? + forward_message :forward_invoke, :setuid?, :setgid? + forward_message :forward_invoke, :zero? + forward_message :forward_invoke, :size, :size? + forward_message :forward_invoke, :mtime, :atime, :ctime + + def blocks; nil; end + + def get_entry + @zipFsFile.__send__(:get_entry, @entryName) + end + private :get_entry + + def gid + e = get_entry + if e.extra.member? "IUnix" + e.extra["IUnix"].gid || 0 + else + 0 + end + end + + def uid + e = get_entry + if e.extra.member? "IUnix" + e.extra["IUnix"].uid || 0 + else + 0 + end + end + + def ino; 0; end + + def dev; 0; end + + def rdev; 0; end + + def rdev_major; 0; end + + def rdev_minor; 0; end + + def ftype + if file? + return "file" + elsif directory? + return "directory" + else + raise StandardError, "Unknown file type" + end + end + + def nlink; 1; end + + def blksize; nil; end + + def mode + e = get_entry + if e.fstype == 3 + e.externalFileAttributes >> 16 + else + 33206 # 33206 is equivalent to -rw-rw-rw- + end + end + end + + def initialize(mappedZip) + @mappedZip = mappedZip + end + + def get_entry(fileName) + if ! exists?(fileName) + raise Errno::ENOENT, "No such file or directory - #{fileName}" + end + @mappedZip.find_entry(fileName) + end + private :get_entry + + def unix_mode_cmp(fileName, mode) + begin + e = get_entry(fileName) + e.fstype == 3 && ((e.externalFileAttributes >> 16) & mode ) != 0 + rescue Errno::ENOENT + false + end + end + private :unix_mode_cmp + + def exists?(fileName) + expand_path(fileName) == "/" || @mappedZip.find_entry(fileName) != nil + end + alias :exist? :exists? + + # Permissions not implemented, so if the file exists it is accessible + alias owned? exists? + alias grpowned? exists? + + def readable?(fileName) + unix_mode_cmp(fileName, 0444) + end + alias readable_real? readable? + + def writable?(fileName) + unix_mode_cmp(fileName, 0222) + end + alias writable_real? writable? + + def executable?(fileName) + unix_mode_cmp(fileName, 0111) + end + alias executable_real? executable? + + def setuid?(fileName) + unix_mode_cmp(fileName, 04000) + end + + def setgid?(fileName) + unix_mode_cmp(fileName, 02000) + end + + def sticky?(fileName) + unix_mode_cmp(fileName, 01000) + end + + def umask(*args) + ::File.umask(*args) + end + + def truncate(fileName, len) + raise StandardError, "truncate not supported" + end + + def directory?(fileName) + entry = @mappedZip.find_entry(fileName) + expand_path(fileName) == "/" || (entry != nil && entry.directory?) + end + + def open(fileName, openMode = "r", &block) + case openMode + when "r" + @mappedZip.get_input_stream(fileName, &block) + when "w" + @mappedZip.get_output_stream(fileName, &block) + else + raise StandardError, "openmode '#{openMode} not supported" unless openMode == "r" + end + end + + def new(fileName, openMode = "r") + open(fileName, openMode) + end + + def size(fileName) + @mappedZip.get_entry(fileName).size + end + + # Returns nil for not found and nil for directories + def size?(fileName) + entry = @mappedZip.find_entry(fileName) + return (entry == nil || entry.directory?) ? nil : entry.size + end + + def chown(ownerInt, groupInt, *filenames) + filenames.each { |fileName| + e = get_entry(fileName) + unless e.extra.member?("IUnix") + e.extra.create("IUnix") + end + e.extra["IUnix"].uid = ownerInt + e.extra["IUnix"].gid = groupInt + } + filenames.size + end + + def chmod (modeInt, *filenames) + filenames.each { |fileName| + e = get_entry(fileName) + e.fstype = 3 # force convertion filesystem type to unix + e.externalFileAttributes = modeInt << 16 + } + filenames.size + end + + def zero?(fileName) + sz = size(fileName) + sz == nil || sz == 0 + rescue Errno::ENOENT + false + end + + def file?(fileName) + entry = @mappedZip.find_entry(fileName) + entry != nil && entry.file? + end + + def dirname(fileName) + ::File.dirname(fileName) + end + + def basename(fileName) + ::File.basename(fileName) + end + + def split(fileName) + ::File.split(fileName) + end + + def join(*fragments) + ::File.join(*fragments) + end + + def utime(modifiedTime, *fileNames) + fileNames.each { |fileName| + get_entry(fileName).time = modifiedTime + } + end + + def mtime(fileName) + @mappedZip.get_entry(fileName).mtime + end + + def atime(fileName) + e = get_entry(fileName) + if e.extra.member? "UniversalTime" + e.extra["UniversalTime"].atime + else + nil + end + end + + def ctime(fileName) + e = get_entry(fileName) + if e.extra.member? "UniversalTime" + e.extra["UniversalTime"].ctime + else + nil + end + end + + def pipe?(filename) + false + end + + def blockdev?(filename) + false + end + + def chardev?(filename) + false + end + + def symlink?(fileName) + false + end + + def socket?(fileName) + false + end + + def ftype(fileName) + @mappedZip.get_entry(fileName).directory? ? "directory" : "file" + end + + def readlink(fileName) + raise NotImplementedError, "The readlink() function is not implemented" + end + + def symlink(fileName, symlinkName) + raise NotImplementedError, "The symlink() function is not implemented" + end + + def link(fileName, symlinkName) + raise NotImplementedError, "The link() function is not implemented" + end + + def pipe + raise NotImplementedError, "The pipe() function is not implemented" + end + + def stat(fileName) + if ! exists?(fileName) + raise Errno::ENOENT, fileName + end + ZipFsStat.new(self, fileName) + end + + alias lstat stat + + def readlines(fileName) + open(fileName) { |is| is.readlines } + end + + def read(fileName) + @mappedZip.read(fileName) + end + + def popen(*args, &aProc) + File.popen(*args, &aProc) + end + + def foreach(fileName, aSep = $/, &aProc) + open(fileName) { |is| is.each_line(aSep, &aProc) } + end + + def delete(*args) + args.each { + |fileName| + if directory?(fileName) + raise Errno::EISDIR, "Is a directory - \"#{fileName}\"" + end + @mappedZip.remove(fileName) + } + end + + def rename(fileToRename, newName) + @mappedZip.rename(fileToRename, newName) { true } + end + + alias :unlink :delete + + def expand_path(aPath) + @mappedZip.expand_path(aPath) + end + end + + # Instances of this class are normally accessed via the accessor + # ZipFile::dir. An instance of ZipFsDir behaves like ruby's + # builtin Dir (class) object, except it works on ZipFile entries. + # + # The individual methods are not documented due to their + # similarity with the methods in Dir + class ZipFsDir + + def initialize(mappedZip) + @mappedZip = mappedZip + end + + attr_writer :file + + def new(aDirectoryName) + ZipFsDirIterator.new(entries(aDirectoryName)) + end + + def open(aDirectoryName) + dirIt = new(aDirectoryName) + if block_given? + begin + yield(dirIt) + return nil + ensure + dirIt.close + end + end + dirIt + end + + def pwd; @mappedZip.pwd; end + alias getwd pwd + + def chdir(aDirectoryName) + unless @file.stat(aDirectoryName).directory? + raise Errno::EINVAL, "Invalid argument - #{aDirectoryName}" + end + @mappedZip.pwd = @file.expand_path(aDirectoryName) + end + + def entries(aDirectoryName) + entries = [] + foreach(aDirectoryName) { |e| entries << e } + entries + end + + def foreach(aDirectoryName) + unless @file.stat(aDirectoryName).directory? + raise Errno::ENOTDIR, aDirectoryName + end + path = @file.expand_path(aDirectoryName).ensure_end("/") + + subDirEntriesRegex = Regexp.new("^#{path}([^/]+)$") + @mappedZip.each { + |fileName| + match = subDirEntriesRegex.match(fileName) + yield(match[1]) unless match == nil + } + end + + def delete(entryName) + unless @file.stat(entryName).directory? + raise Errno::EINVAL, "Invalid argument - #{entryName}" + end + @mappedZip.remove(entryName) + end + alias rmdir delete + alias unlink delete + + def mkdir(entryName, permissionInt = 0755) + @mappedZip.mkdir(entryName, permissionInt) + end + + def chroot(*args) + raise NotImplementedError, "The chroot() function is not implemented" + end + + end + + class ZipFsDirIterator # :nodoc:all + include Enumerable + + def initialize(arrayOfFileNames) + @fileNames = arrayOfFileNames + @index = 0 + end + + def close + @fileNames = nil + end + + def each(&aProc) + raise IOError, "closed directory" if @fileNames == nil + @fileNames.each(&aProc) + end + + def read + raise IOError, "closed directory" if @fileNames == nil + @fileNames[(@index+=1)-1] + end + + def rewind + raise IOError, "closed directory" if @fileNames == nil + @index = 0 + end + + def seek(anIntegerPosition) + raise IOError, "closed directory" if @fileNames == nil + @index = anIntegerPosition + end + + def tell + raise IOError, "closed directory" if @fileNames == nil + @index + end + end + + # All access to ZipFile from ZipFsFile and ZipFsDir goes through a + # ZipFileNameMapper, which has one responsibility: ensure + class ZipFileNameMapper # :nodoc:all + include Enumerable + + def initialize(zipFile) + @zipFile = zipFile + @pwd = "/" + end + + attr_accessor :pwd + + def find_entry(fileName) + @zipFile.find_entry(expand_to_entry(fileName)) + end + + def get_entry(fileName) + @zipFile.get_entry(expand_to_entry(fileName)) + end + + def get_input_stream(fileName, &aProc) + @zipFile.get_input_stream(expand_to_entry(fileName), &aProc) + end + + def get_output_stream(fileName, &aProc) + @zipFile.get_output_stream(expand_to_entry(fileName), &aProc) + end + + def read(fileName) + @zipFile.read(expand_to_entry(fileName)) + end + + def remove(fileName) + @zipFile.remove(expand_to_entry(fileName)) + end + + def rename(fileName, newName, &continueOnExistsProc) + @zipFile.rename(expand_to_entry(fileName), expand_to_entry(newName), + &continueOnExistsProc) + end + + def mkdir(fileName, permissionInt = 0755) + @zipFile.mkdir(expand_to_entry(fileName), permissionInt) + end + + # Turns entries into strings and adds leading / + # and removes trailing slash on directories + def each + @zipFile.each { + |e| + yield("/"+e.to_s.chomp("/")) + } + end + + def expand_path(aPath) + expanded = aPath.starts_with("/") ? aPath : @pwd.ensure_end("/") + aPath + expanded.gsub!(/\/\.(\/|$)/, "") + expanded.gsub!(/[^\/]+\/\.\.(\/|$)/, "") + expanded.empty? ? "/" : expanded + end + + private + + def expand_to_entry(aPath) + expand_path(aPath).lchop + end + end + end + + class ZipFile + include ZipFileSystem + end +end + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/lib/zip/ziprequire.rb b/vendor/plugins/rubyzip-0.9.1/lib/zip/ziprequire.rb new file mode 100755 index 00000000..5a4c4d48 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/lib/zip/ziprequire.rb @@ -0,0 +1,90 @@ +# With ziprequire you can load ruby modules from a zip file. This means +# ruby's module include path can include zip-files. +# +# The following example creates a zip file with a single entry +# log/simplelog.rb that contains a single function +# simpleLog: +# +# require 'zip/zipfilesystem' +# +# Zip::ZipFile.open("my.zip", true) { +# |zf| +# zf.file.open("log/simplelog.rb", "w") { +# |f| +# f.puts "def simpleLog(v)" +# f.puts ' Kernel.puts "INFO: #{v}"' +# f.puts "end" +# } +# } +# +# To use the ruby module stored in the zip archive simply require +# zip/ziprequire and include the my.zip zip +# file in the module search path. The following command shows one +# way to do this: +# +# ruby -rzip/ziprequire -Imy.zip -e " require 'log/simplelog'; simpleLog 'Hello world' " + +#$: << 'data/rubycode.zip' << 'data/rubycode2.zip' + + +require 'zip/zip' + +class ZipList #:nodoc:all + def initialize(zipFileList) + @zipFileList = zipFileList + end + + def get_input_stream(entry, &aProc) + @zipFileList.each { + |zfName| + Zip::ZipFile.open(zfName) { + |zf| + begin + return zf.get_input_stream(entry, &aProc) + rescue Errno::ENOENT + end + } + } + raise Errno::ENOENT, + "No matching entry found in zip files '#{@zipFileList.join(', ')}' "+ + " for '#{entry}'" + end +end + + +module Kernel #:nodoc:all + alias :oldRequire :require + + def require(moduleName) + zip_require(moduleName) || oldRequire(moduleName) + end + + def zip_require(moduleName) + return false if already_loaded?(moduleName) + get_resource(ensure_rb_extension(moduleName)) { + |zis| + eval(zis.read); $" << moduleName + } + return true + rescue Errno::ENOENT => ex + return false + end + + def get_resource(resourceName, &aProc) + zl = ZipList.new($:.grep(/\.zip$/)) + zl.get_input_stream(resourceName, &aProc) + end + + def already_loaded?(moduleName) + moduleRE = Regexp.new("^"+moduleName+"(\.rb|\.so|\.dll|\.o)?$") + $".detect { |e| e =~ moduleRE } != nil + end + + def ensure_rb_extension(aString) + aString.sub(/(\.rb)?$/i, ".rb") + end +end + +# Copyright (C) 2002 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/samples/example.rb b/vendor/plugins/rubyzip-0.9.1/samples/example.rb new file mode 100755 index 00000000..741afa76 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/samples/example.rb @@ -0,0 +1,69 @@ +#!/usr/bin/env ruby + +$: << "../lib" +system("zip example.zip example.rb gtkRubyzip.rb") + +require 'zip/zip' + +####### Using ZipInputStream alone: ####### + +Zip::ZipInputStream.open("example.zip") { + |zis| + entry = zis.get_next_entry + print "First line of '#{entry.name} (#{entry.size} bytes): " + puts "'#{zis.gets.chomp}'" + entry = zis.get_next_entry + print "First line of '#{entry.name} (#{entry.size} bytes): " + puts "'#{zis.gets.chomp}'" +} + + +####### Using ZipFile to read the directory of a zip file: ####### + +zf = Zip::ZipFile.new("example.zip") +zf.each_with_index { + |entry, index| + + puts "entry #{index} is #{entry.name}, size = #{entry.size}, compressed size = #{entry.compressed_size}" + # use zf.get_input_stream(entry) to get a ZipInputStream for the entry + # entry can be the ZipEntry object or any object which has a to_s method that + # returns the name of the entry. +} + + +####### Using ZipOutputStream to write a zip file: ####### + +Zip::ZipOutputStream.open("exampleout.zip") { + |zos| + zos.put_next_entry("the first little entry") + zos.puts "Hello hello hello hello hello hello hello hello hello" + + zos.put_next_entry("the second little entry") + zos.puts "Hello again" + + # Use rubyzip or your zip client of choice to verify + # the contents of exampleout.zip +} + +####### Using ZipFile to change a zip file: ####### + +Zip::ZipFile.open("exampleout.zip") { + |zf| + zf.add("thisFile.rb", "example.rb") + zf.rename("thisFile.rb", "ILikeThisName.rb") + zf.add("Again", "example.rb") +} + +# Lets check +Zip::ZipFile.open("exampleout.zip") { + |zf| + puts "Changed zip file contains: #{zf.entries.join(', ')}" + zf.remove("Again") + puts "Without 'Again': #{zf.entries.join(', ')}" +} + +# For other examples, look at zip.rb and ziptest.rb + +# Copyright (C) 2002 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/samples/example_filesystem.rb b/vendor/plugins/rubyzip-0.9.1/samples/example_filesystem.rb new file mode 100755 index 00000000..867e8d4f --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/samples/example_filesystem.rb @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby + +$: << "../lib" + +require 'zip/zipfilesystem' +require 'ftools' + +EXAMPLE_ZIP = "filesystem.zip" + +File.delete(EXAMPLE_ZIP) if File.exists?(EXAMPLE_ZIP) + +Zip::ZipFile.open(EXAMPLE_ZIP, Zip::ZipFile::CREATE) { + |zf| + zf.file.open("file1.txt", "w") { |os| os.write "first file1.txt" } + zf.dir.mkdir("dir1") + zf.dir.chdir("dir1") + zf.file.open("file1.txt", "w") { |os| os.write "second file1.txt" } + puts zf.file.read("file1.txt") + puts zf.file.read("../file1.txt") + zf.dir.chdir("..") + zf.file.open("file2.txt", "w") { |os| os.write "first file2.txt" } + puts "Entries: #{zf.entries.join(', ')}" +} + +Zip::ZipFile.open(EXAMPLE_ZIP) { + |zf| + puts "Entries from reloaded zip: #{zf.entries.join(', ')}" +} + +# For other examples, look at zip.rb and ziptest.rb + +# Copyright (C) 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/samples/gtkRubyzip.rb b/vendor/plugins/rubyzip-0.9.1/samples/gtkRubyzip.rb new file mode 100755 index 00000000..5d91829d --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/samples/gtkRubyzip.rb @@ -0,0 +1,86 @@ +#!/usr/bin/env ruby + +$: << "../lib" + +$VERBOSE = true + +require 'gtk' +require 'zip/zip' + +class MainApp < Gtk::Window + def initialize + super() + set_usize(400, 256) + set_title("rubyzip") + signal_connect(Gtk::Window::SIGNAL_DESTROY) { Gtk.main_quit } + + box = Gtk::VBox.new(false, 0) + add(box) + + @zipfile = nil + @buttonPanel = ButtonPanel.new + @buttonPanel.openButton.signal_connect(Gtk::Button::SIGNAL_CLICKED) { + show_file_selector + } + @buttonPanel.extractButton.signal_connect(Gtk::Button::SIGNAL_CLICKED) { + puts "Not implemented!" + } + box.pack_start(@buttonPanel, false, false, 0) + + sw = Gtk::ScrolledWindow.new + sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC) + box.pack_start(sw, true, true, 0) + + @clist = Gtk::CList.new(["Name", "Size", "Compression"]) + @clist.set_selection_mode(Gtk::SELECTION_BROWSE) + @clist.set_column_width(0, 120) + @clist.set_column_width(1, 120) + @clist.signal_connect(Gtk::CList::SIGNAL_SELECT_ROW) { + |w, row, column, event| + @selected_row = row + } + sw.add(@clist) + end + + class ButtonPanel < Gtk::HButtonBox + attr_reader :openButton, :extractButton + def initialize + super + set_layout(Gtk::BUTTONBOX_START) + set_spacing(0) + @openButton = Gtk::Button.new("Open archive") + @extractButton = Gtk::Button.new("Extract entry") + pack_start(@openButton) + pack_start(@extractButton) + end + end + + def show_file_selector + @fileSelector = Gtk::FileSelection.new("Open zip file") + @fileSelector.show + @fileSelector.ok_button.signal_connect(Gtk::Button::SIGNAL_CLICKED) { + open_zip(@fileSelector.filename) + @fileSelector.destroy + } + @fileSelector.cancel_button.signal_connect(Gtk::Button::SIGNAL_CLICKED) { + @fileSelector.destroy + } + end + + def open_zip(filename) + @zipfile = Zip::ZipFile.open(filename) + @clist.clear + @zipfile.each { + |entry| + @clist.append([ entry.name, + entry.size.to_s, + (100.0*entry.compressedSize/entry.size).to_s+"%" ]) + } + end +end + +mainApp = MainApp.new() + +mainApp.show_all + +Gtk.main diff --git a/vendor/plugins/rubyzip-0.9.1/samples/qtzip.rb b/vendor/plugins/rubyzip-0.9.1/samples/qtzip.rb new file mode 100755 index 00000000..3d76bd18 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/samples/qtzip.rb @@ -0,0 +1,101 @@ +#!/usr/bin/env ruby + +$VERBOSE=true + +$: << "../lib" + +require 'Qt' +system('rbuic -o zipdialogui.rb zipdialogui.ui') +require 'zipdialogui.rb' +require 'zip/zip' + + + +a = Qt::Application.new(ARGV) + +class ZipDialog < ZipDialogUI + + + def initialize() + super() + connect(child('add_button'), SIGNAL('clicked()'), + self, SLOT('add_files()')) + connect(child('extract_button'), SIGNAL('clicked()'), + self, SLOT('extract_files()')) + end + + def zipfile(&proc) + Zip::ZipFile.open(@zip_filename, &proc) + end + + def each(&proc) + Zip::ZipFile.foreach(@zip_filename, &proc) + end + + def refresh() + lv = child("entry_list_view") + lv.clear + each { + |e| + lv.insert_item(Qt::ListViewItem.new(lv, e.name, e.size.to_s)) + } + end + + + def load(zipfile) + @zip_filename = zipfile + refresh + end + + def add_files + l = Qt::FileDialog.getOpenFileNames(nil, nil, self) + zipfile { + |zf| + l.each { + |path| + zf.add(File.basename(path), path) + } + } + refresh + end + + def extract_files + selected_items = [] + unselected_items = [] + lv_item = entry_list_view.first_child + while (lv_item) + if entry_list_view.is_selected(lv_item) + selected_items << lv_item.text(0) + else + unselected_items << lv_item.text(0) + end + lv_item = lv_item.next_sibling + end + puts "selected_items.size = #{selected_items.size}" + puts "unselected_items.size = #{unselected_items.size}" + items = selected_items.size > 0 ? selected_items : unselected_items + puts "items.size = #{items.size}" + + d = Qt::FileDialog.get_existing_directory(nil, self) + if (!d) + puts "No directory chosen" + else + zipfile { |zf| items.each { |e| zf.extract(e, File.join(d, e)) } } + end + + end + + slots 'add_files()', 'extract_files()' +end + +if !ARGV[0] + puts "usage: #{$0} zipname" + exit +end + +zd = ZipDialog.new +zd.load(ARGV[0]) + +a.mainWidget = zd +zd.show() +a.exec() diff --git a/vendor/plugins/rubyzip-0.9.1/samples/write_simple.rb b/vendor/plugins/rubyzip-0.9.1/samples/write_simple.rb new file mode 100755 index 00000000..5a1f26b1 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/samples/write_simple.rb @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby + +$: << "../lib" + +require 'zip/zip' + +include Zip + +ZipOutputStream.open('simple.zip') { + |zos| + ze = zos.put_next_entry 'entry.txt' + zos.puts "Hello world" +} diff --git a/vendor/plugins/rubyzip-0.9.1/samples/zipfind.rb b/vendor/plugins/rubyzip-0.9.1/samples/zipfind.rb new file mode 100755 index 00000000..54ad936e --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/samples/zipfind.rb @@ -0,0 +1,74 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +$: << "../lib" + +require 'zip/zip' +require 'find' + +module Zip + module ZipFind + def self.find(path, zipFilePattern = /\.zip$/i) + Find.find(path) { + |fileName| + yield(fileName) + if zipFilePattern.match(fileName) && File.file?(fileName) + begin + Zip::ZipFile.foreach(fileName) { + |zipEntry| + yield(fileName + File::SEPARATOR + zipEntry.to_s) + } + rescue Errno::EACCES => ex + puts ex + end + end + } + end + + def self.find_file(path, fileNamePattern, zipFilePattern = /\.zip$/i) + self.find(path, zipFilePattern) { + |fileName| + yield(fileName) if fileNamePattern.match(fileName) + } + end + + end +end + +if __FILE__ == $0 + module ZipFindConsoleRunner + + PATH_ARG_INDEX = 0; + FILENAME_PATTERN_ARG_INDEX = 1; + ZIPFILE_PATTERN_ARG_INDEX = 2; + + def self.run(args) + check_args(args) + Zip::ZipFind.find_file(args[PATH_ARG_INDEX], + args[FILENAME_PATTERN_ARG_INDEX], + args[ZIPFILE_PATTERN_ARG_INDEX]) { + |fileName| + report_entry_found fileName + } + end + + def self.check_args(args) + if (args.size != 3) + usage + exit + end + end + + def self.usage + puts "Usage: #{$0} PATH ZIPFILENAME_PATTERN FILNAME_PATTERN" + end + + def self.report_entry_found(fileName) + puts fileName + end + + end + + ZipFindConsoleRunner.run(ARGV) +end diff --git a/vendor/plugins/rubyzip-0.9.1/test/alltests.rb b/vendor/plugins/rubyzip-0.9.1/test/alltests.rb new file mode 100755 index 00000000..691349af --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/test/alltests.rb @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +require 'stdrubyexttest' +require 'ioextrastest' +require 'ziptest' +require 'zipfilesystemtest' +require 'ziprequiretest' diff --git a/vendor/plugins/rubyzip-0.9.1/test/data/file1.txt b/vendor/plugins/rubyzip-0.9.1/test/data/file1.txt new file mode 100644 index 00000000..23ea2f73 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/test/data/file1.txt @@ -0,0 +1,46 @@ + +AUTOMAKE_OPTIONS = gnu + +EXTRA_DIST = test.zip + +CXXFLAGS= -g + +noinst_LIBRARIES = libzipios.a + +bin_PROGRAMS = test_zip test_izipfilt test_izipstream +# test_flist + +libzipios_a_SOURCES = backbuffer.h fcol.cpp fcol.h \ + fcol_common.h fcolexceptions.cpp fcolexceptions.h \ + fileentry.cpp fileentry.h flist.cpp \ + flist.h flistentry.cpp flistentry.h \ + flistscanner.h ifiltstreambuf.cpp ifiltstreambuf.h \ + inflatefilt.cpp inflatefilt.h izipfilt.cpp \ + izipfilt.h izipstream.cpp izipstream.h \ + zipfile.cpp zipfile.h ziphead.cpp \ + ziphead.h flistscanner.ll + +# test_flist_SOURCES = test_flist.cpp + +test_izipfilt_SOURCES = test_izipfilt.cpp + +test_izipstream_SOURCES = test_izipstream.cpp + +test_zip_SOURCES = test_zip.cpp + +# Notice that libzipios.a is not specified as -L. -lzipios +# If it was, automake would not include it as a dependency. + +# test_flist_LDADD = libzipios.a + +test_izipfilt_LDADD = libzipios.a -lz + +test_zip_LDADD = libzipios.a -lz + +test_izipstream_LDADD = libzipios.a -lz + + + +flistscanner.cc : flistscanner.ll + $(LEX) -+ -PFListScanner -o$@ $^ + diff --git a/vendor/plugins/rubyzip-0.9.1/test/data/file1.txt.deflatedData b/vendor/plugins/rubyzip-0.9.1/test/data/file1.txt.deflatedData new file mode 100644 index 00000000..bfbb4f42 Binary files /dev/null and b/vendor/plugins/rubyzip-0.9.1/test/data/file1.txt.deflatedData differ diff --git a/vendor/plugins/rubyzip-0.9.1/test/data/file2.txt b/vendor/plugins/rubyzip-0.9.1/test/data/file2.txt new file mode 100644 index 00000000..cc9ef6ad --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/test/data/file2.txt @@ -0,0 +1,1504 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +require 'rubyunit' +require 'zip' + +include Zip + +Dir.chdir "test" + +class AbstractInputStreamTest < RUNIT::TestCase + # AbstractInputStream subclass that provides a read method + + TEST_LINES = [ "Hello world#{$/}", + "this is the second line#{$/}", + "this is the last line"] + TEST_STRING = TEST_LINES.join + class TestAbstractInputStream + include AbstractInputStream + def initialize(aString) + @contents = aString + @readPointer = 0 + end + + def read(charsToRead) + retVal=@contents[@readPointer, charsToRead] + @readPointer+=charsToRead + return retVal + end + + def produceInput + read(100) + end + + def inputFinished? + @contents[@readPointer] == nil + end + end + + def setup + @io = TestAbstractInputStream.new(TEST_STRING) + end + + def test_gets + assert_equals(TEST_LINES[0], @io.gets) + assert_equals(TEST_LINES[1], @io.gets) + assert_equals(TEST_LINES[2], @io.gets) + assert_equals(nil, @io.gets) + end + + def test_getsMultiCharSeperator + assert_equals("Hell", @io.gets("ll")) + assert_equals("o world#{$/}this is the second l", @io.gets("d l")) + end + + def test_each_line + lineNumber=0 + @io.each_line { + |line| + assert_equals(TEST_LINES[lineNumber], line) + lineNumber+=1 + } + end + + def test_readlines + assert_equals(TEST_LINES, @io.readlines) + end + + def test_readline + test_gets + begin + @io.readline + fail "EOFError expected" + rescue EOFError + end + end +end + +class ZipEntryTest < RUNIT::TestCase + TEST_ZIPFILE = "someZipFile.zip" + TEST_COMMENT = "a comment" + TEST_COMPRESSED_SIZE = 1234 + TEST_CRC = 325324 + TEST_EXTRA = "Some data here" + TEST_COMPRESSIONMETHOD = ZipEntry::DEFLATED + TEST_NAME = "entry name" + TEST_SIZE = 8432 + TEST_ISDIRECTORY = false + + def test_constructorAndGetters + entry = ZipEntry.new(TEST_ZIPFILE, + TEST_NAME, + TEST_COMMENT, + TEST_EXTRA, + TEST_COMPRESSED_SIZE, + TEST_CRC, + TEST_COMPRESSIONMETHOD, + TEST_SIZE) + + assert_equals(TEST_COMMENT, entry.comment) + assert_equals(TEST_COMPRESSED_SIZE, entry.compressedSize) + assert_equals(TEST_CRC, entry.crc) + assert_equals(TEST_EXTRA, entry.extra) + assert_equals(TEST_COMPRESSIONMETHOD, entry.compressionMethod) + assert_equals(TEST_NAME, entry.name) + assert_equals(TEST_SIZE, entry.size) + assert_equals(TEST_ISDIRECTORY, entry.isDirectory) + end + + def test_equality + entry1 = ZipEntry.new("file.zip", "name", "isNotCompared", + "something extra", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry2 = ZipEntry.new("file.zip", "name", "isNotComparedXXX", + "something extra", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry3 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extra", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry4 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry5 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 1234, + ZipEntry::DEFLATED, 10000) + entry6 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 123, + ZipEntry::DEFLATED, 10000) + entry7 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 123, + ZipEntry::STORED, 10000) + entry8 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 123, + ZipEntry::STORED, 100000) + + assert_equals(entry1, entry1) + assert_equals(entry1, entry2) + + assert(entry2 != entry3) + assert(entry3 != entry4) + assert(entry4 != entry5) + assert(entry5 != entry6) + assert(entry6 != entry7) + assert(entry7 != entry8) + + assert(entry7 != "hello") + assert(entry7 != 12) + end +end + +module IOizeString + attr_reader :tell + + def read(count = nil) + @tell ||= 0 + count = size unless count + retVal = slice(@tell, count) + @tell += count + return retVal + end + + def seek(index, offset) + @tell ||= 0 + case offset + when IO::SEEK_END + newPos = size + index + when IO::SEEK_SET + newPos = index + when IO::SEEK_CUR + newPos = @tell + index + else + raise "Error in test method IOizeString::seek" + end + if (newPos < 0 || newPos >= size) + raise Errno::EINVAL + else + @tell=newPos + end + end + + def reset + @tell = 0 + end +end + +class ZipLocalEntryTest < RUNIT::TestCase + def test_readLocalEntryHeaderOfFirstTestZipEntry + File.open(TestZipFile::TEST_ZIP3.zipName) { + |file| + entry = ZipEntry.readLocalEntry(file) + + assert_equal("", entry.comment) + # Differs from windows and unix because of CR LF + # assert_equal(480, entry.compressedSize) + # assert_equal(0x2a27930f, entry.crc) + # extra field is 21 bytes long + # probably contains some unix attrutes or something + # disabled: assert_equal(nil, entry.extra) + assert_equal(ZipEntry::DEFLATED, entry.compressionMethod) + assert_equal(TestZipFile::TEST_ZIP3.entryNames[0], entry.name) + assert_equal(File.size(TestZipFile::TEST_ZIP3.entryNames[0]), entry.size) + assert(! entry.isDirectory) + } + end + + def test_readLocalEntryFromNonZipFile + File.open("ziptest.rb") { + |file| + assert_equals(nil, ZipEntry.readLocalEntry(file)) + } + end + + def test_readLocalEntryFromTruncatedZipFile + zipFragment="" + File.open(TestZipFile::TEST_ZIP2.zipName) { |f| zipFragment = f.read(12) } # local header is at least 30 bytes + zipFragment.extend(IOizeString).reset + entry = ZipEntry.new + entry.readLocalEntry(zipFragment) + fail "ZipError expected" + rescue ZipError + end + + def test_writeEntry + entry = ZipEntry.new("file.zip", "entryName", "my little comment", + "thisIsSomeExtraInformation", 100, 987654, + ZipEntry::DEFLATED, 400) + writeToFile("localEntryHeader.bin", "centralEntryHeader.bin", entry) + entryReadLocal, entryReadCentral = readFromFile("localEntryHeader.bin", "centralEntryHeader.bin") + compareLocalEntryHeaders(entry, entryReadLocal) + compareCDirEntryHeaders(entry, entryReadCentral) + end + + private + def compareLocalEntryHeaders(entry1, entry2) + assert_equals(entry1.compressedSize , entry2.compressedSize) + assert_equals(entry1.crc , entry2.crc) + assert_equals(entry1.extra , entry2.extra) + assert_equals(entry1.compressionMethod, entry2.compressionMethod) + assert_equals(entry1.name , entry2.name) + assert_equals(entry1.size , entry2.size) + assert_equals(entry1.localHeaderOffset, entry2.localHeaderOffset) + end + + def compareCDirEntryHeaders(entry1, entry2) + compareLocalEntryHeaders(entry1, entry2) + assert_equals(entry1.comment, entry2.comment) + end + + def writeToFile(localFileName, centralFileName, entry) + File.open(localFileName, "wb") { |f| entry.writeLocalEntry(f) } + File.open(centralFileName, "wb") { |f| entry.writeCDirEntry(f) } + end + + def readFromFile(localFileName, centralFileName) + localEntry = nil + cdirEntry = nil + File.open(localFileName, "rb") { |f| localEntry = ZipEntry.readLocalEntry(f) } + File.open(centralFileName, "rb") { |f| cdirEntry = ZipEntry.readCDirEntry(f) } + return [localEntry, cdirEntry] + end +end + + +module DecompressorTests + # expects @refText and @decompressor + + def test_readEverything + assert_equals(@refText, @decompressor.read) + end + + def test_readInChunks + chunkSize = 5 + while (decompressedChunk = @decompressor.read(chunkSize)) + assert_equals(@refText.slice!(0, chunkSize), decompressedChunk) + end + assert_equals(0, @refText.size) + end +end + +class InflaterTest < RUNIT::TestCase + include DecompressorTests + + def setup + @file = File.new("file1.txt.deflatedData", "rb") + @refText="" + File.open("file1.txt") { |f| @refText = f.read } + @decompressor = Inflater.new(@file) + end + + def teardown + @file.close + end +end + + +class PassThruDecompressorTest < RUNIT::TestCase + include DecompressorTests + TEST_FILE="file1.txt" + def setup + @file = File.new(TEST_FILE) + @refText="" + File.open(TEST_FILE) { |f| @refText = f.read } + @decompressor = PassThruDecompressor.new(@file, File.size(TEST_FILE)) + end + + def teardown + @file.close + end +end + + +module AssertEntry + def assertNextEntry(filename, zis) + assertEntry(filename, zis, zis.getNextEntry.name) + end + + def assertEntry(filename, zis, entryName) + assert_equals(filename, entryName) + assertEntryContentsForStream(filename, zis, entryName) + end + + def assertEntryContentsForStream(filename, zis, entryName) + File.open(filename, "rb") { + |file| + expected = file.read + actual = zis.read + if (expected != actual) + if (expected.length > 400 || actual.length > 400) + zipEntryFilename=entryName+".zipEntry" + File.open(zipEntryFilename, "wb") { |file| file << actual } + fail("File '#{filename}' is different from '#{zipEntryFilename}'") + else + assert_equals(expected, actual) + end + end + } + end + + def AssertEntry.assertContents(filename, aString) + fileContents = "" + File.open(filename, "rb") { |f| fileContents = f.read } + if (fileContents != aString) + if (expected.length > 400 || actual.length > 400) + stringFile = filename + ".other" + File.open(stringFile, "wb") { |f| f << aString } + fail("File '#{filename}' is different from contents of string stored in '#{stringFile}'") + else + assert_equals(expected, actual) + end + end + end + + def assertStreamContents(zis, testZipFile) + assert(zis != nil) + testZipFile.entryNames.each { + |entryName| + assertNextEntry(entryName, zis) + } + assert_equals(nil, zis.getNextEntry) + end + + def assertTestZipContents(testZipFile) + ZipInputStream.open(testZipFile.zipName) { + |zis| + assertStreamContents(zis, testZipFile) + } + end + + def assertEntryContents(zipFile, entryName, filename = entryName.to_s) + zis = zipFile.getInputStream(entryName) + assertEntryContentsForStream(filename, zis, entryName) + ensure + zis.close if zis + end +end + + + +class ZipInputStreamTest < RUNIT::TestCase + include AssertEntry + + def test_new + zis = ZipInputStream.new(TestZipFile::TEST_ZIP2.zipName) + assertStreamContents(zis, TestZipFile::TEST_ZIP2) + zis.close + end + + def test_openWithBlock + ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) { + |zis| + assertStreamContents(zis, TestZipFile::TEST_ZIP2) + } + end + + def test_openWithoutBlock + zis = ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) + assertStreamContents(zis, TestZipFile::TEST_ZIP2) + end + + def test_incompleteReads + ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) { + |zis| + entry = zis.getNextEntry + assert_equals(TestZipFile::TEST_ZIP2.entryNames[0], entry.name) + assert zis.gets.length > 0 + entry = zis.getNextEntry + assert_equals(TestZipFile::TEST_ZIP2.entryNames[1], entry.name) + assert_equals(0, entry.size) + assert_equals(nil, zis.gets) + entry = zis.getNextEntry + assert_equals(TestZipFile::TEST_ZIP2.entryNames[2], entry.name) + assert zis.gets.length > 0 + entry = zis.getNextEntry + assert_equals(TestZipFile::TEST_ZIP2.entryNames[3], entry.name) + assert zis.gets.length > 0 + } + end + +end + +class TestFiles + RANDOM_ASCII_FILE1 = "randomAscii1.txt" + RANDOM_ASCII_FILE2 = "randomAscii2.txt" + RANDOM_ASCII_FILE3 = "randomAscii3.txt" + RANDOM_BINARY_FILE1 = "randomBinary1.bin" + RANDOM_BINARY_FILE2 = "randomBinary2.bin" + + EMPTY_TEST_DIR = "emptytestdir" + + ASCII_TEST_FILES = [ RANDOM_ASCII_FILE1, RANDOM_ASCII_FILE2, RANDOM_ASCII_FILE3 ] + BINARY_TEST_FILES = [ RANDOM_BINARY_FILE1, RANDOM_BINARY_FILE2 ] + TEST_DIRECTORIES = [ EMPTY_TEST_DIR ] + TEST_FILES = [ ASCII_TEST_FILES, BINARY_TEST_FILES, EMPTY_TEST_DIR ].flatten! + + def TestFiles.createTestFiles(recreate) + if (recreate || + ! (TEST_FILES.inject(true) { |accum, element| accum && File.exists?(element) })) + + ASCII_TEST_FILES.each_with_index { + |filename, index| + createRandomAscii(filename, 1E4 * (index+1)) + } + + BINARY_TEST_FILES.each_with_index { + |filename, index| + createRandomBinary(filename, 1E4 * (index+1)) + } + + ensureDir(EMPTY_TEST_DIR) + end + end + + private + def TestFiles.createRandomAscii(filename, size) + File.open(filename, "wb") { + |file| + while (file.tell < size) + file << rand + end + } + end + + def TestFiles.createRandomBinary(filename, size) + File.open(filename, "wb") { + |file| + while (file.tell < size) + file << rand.to_a.pack("V") + end + } + end + + def TestFiles.ensureDir(name) + if File.exists?(name) + return if File.stat(name).directory? + File.delete(name) + end + Dir.mkdir(name) + end + +end + +# For representation and creation of +# test data +class TestZipFile + attr_accessor :zipName, :entryNames, :comment + + def initialize(zipName, entryNames, comment = "") + @zipName=zipName + @entryNames=entryNames + @comment = comment + end + + def TestZipFile.createTestZips(recreate) + files = Dir.entries(".") + if (recreate || + ! (files.index(TEST_ZIP1.zipName) && + files.index(TEST_ZIP2.zipName) && + files.index(TEST_ZIP3.zipName) && + files.index(TEST_ZIP4.zipName) && + files.index("empty.txt") && + files.index("short.txt") && + files.index("longAscii.txt") && + files.index("longBinary.bin") )) + raise "failed to create test zip '#{TEST_ZIP1.zipName}'" unless + system("zip #{TEST_ZIP1.zipName} ziptest.rb") + raise "failed to remove entry from '#{TEST_ZIP1.zipName}'" unless + system("zip #{TEST_ZIP1.zipName} -d ziptest.rb") + + File.open("empty.txt", "w") {} + + File.open("short.txt", "w") { |file| file << "ABCDEF" } + ziptestTxt="" + File.open("ziptest.rb") { |file| ziptestTxt=file.read } + File.open("longAscii.txt", "w") { + |file| + while (file.tell < 1E5) + file << ziptestTxt + end + } + + testBinaryPattern="" + File.open("empty.zip") { |file| testBinaryPattern=file.read } + testBinaryPattern *= 4 + + File.open("longBinary.bin", "wb") { + |file| + while (file.tell < 3E5) + file << testBinaryPattern << rand + end + } + raise "failed to create test zip '#{TEST_ZIP2.zipName}'" unless + system("zip #{TEST_ZIP2.zipName} #{TEST_ZIP2.entryNames.join(' ')}") + + # without bash system interprets everything after echo as parameters to + # echo including | zip -z ... + raise "failed to add comment to test zip '#{TEST_ZIP2.zipName}'" unless + system("bash -c \"echo #{TEST_ZIP2.comment} | zip -z #{TEST_ZIP2.zipName}\"") + + raise "failed to create test zip '#{TEST_ZIP3.zipName}'" unless + system("zip #{TEST_ZIP3.zipName} #{TEST_ZIP3.entryNames.join(' ')}") + + raise "failed to create test zip '#{TEST_ZIP4.zipName}'" unless + system("zip #{TEST_ZIP4.zipName} #{TEST_ZIP4.entryNames.join(' ')}") + end + rescue + raise $!.to_s + + "\n\nziptest.rb requires the Info-ZIP program 'zip' in the path\n" + + "to create test data. If you don't have it you can download\n" + + "the necessary test files at http://sf.net/projects/rubyzip." + end + + TEST_ZIP1 = TestZipFile.new("empty.zip", []) + TEST_ZIP2 = TestZipFile.new("4entry.zip", %w{ longAscii.txt empty.txt short.txt longBinary.bin}, + "my zip comment") + TEST_ZIP3 = TestZipFile.new("test1.zip", %w{ file1.txt }) + TEST_ZIP4 = TestZipFile.new("zipWithDir.zip", [ "file1.txt", + TestFiles::EMPTY_TEST_DIR]) +end + + +class AbstractOutputStreamTest < RUNIT::TestCase + class TestOutputStream + include AbstractOutputStream + + attr_accessor :buffer + + def initialize + @buffer = "" + end + + def << (data) + @buffer << data + self + end + end + + def setup + @outputStream = TestOutputStream.new + + @origCommaSep = $, + @origOutputSep = $\ + end + + def teardown + $, = @origCommaSep + $\ = @origOutputSep + end + + def test_write + count = @outputStream.write("a little string") + assert_equals("a little string", @outputStream.buffer) + assert_equals("a little string".length, count) + + count = @outputStream.write(". a little more") + assert_equals("a little string. a little more", @outputStream.buffer) + assert_equals(". a little more".length, count) + end + + def test_print + $\ = nil # record separator set to nil + @outputStream.print("hello") + assert_equals("hello", @outputStream.buffer) + + @outputStream.print(" world.") + assert_equals("hello world.", @outputStream.buffer) + + @outputStream.print(" You ok ", "out ", "there?") + assert_equals("hello world. You ok out there?", @outputStream.buffer) + + $\ = "\n" + @outputStream.print + assert_equals("hello world. You ok out there?\n", @outputStream.buffer) + + @outputStream.print("I sure hope so!") + assert_equals("hello world. You ok out there?\nI sure hope so!\n", @outputStream.buffer) + + $, = "X" + @outputStream.buffer = "" + @outputStream.print("monkey", "duck", "zebra") + assert_equals("monkeyXduckXzebra\n", @outputStream.buffer) + + $\ = nil + @outputStream.buffer = "" + @outputStream.print(20) + assert_equals("20", @outputStream.buffer) + end + + def test_printf + @outputStream.printf("%d %04x", 123, 123) + assert_equals("123 007b", @outputStream.buffer) + end + + def test_putc + @outputStream.putc("A") + assert_equals("A", @outputStream.buffer) + @outputStream.putc(65) + assert_equals("AA", @outputStream.buffer) + end + + def test_puts + @outputStream.puts + assert_equals("\n", @outputStream.buffer) + + @outputStream.puts("hello", "world") + assert_equals("\nhello\nworld\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts("hello\n", "world\n") + assert_equals("hello\nworld\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts(["hello\n", "world\n"]) + assert_equals("hello\nworld\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts(["hello\n", "world\n"], "bingo") + assert_equals("hello\nworld\nbingo\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts(16, 20, 50, "hello") + assert_equals("16\n20\n50\nhello\n", @outputStream.buffer) + end +end + + +module CrcTest + def runCrcTest(compressorClass) + str = "Here's a nice little text to compute the crc for! Ho hum, it is nice nice nice nice indeed." + fakeOut = AbstractOutputStreamTest::TestOutputStream.new + + deflater = compressorClass.new(fakeOut) + deflater << str + assert_equals(0x919920fc, deflater.crc) + end +end + + + +class PassThruCompressorTest < RUNIT::TestCase + include CrcTest + + def test_size + File.open("dummy.txt", "wb") { + |file| + compressor = PassThruCompressor.new(file) + + assert_equals(0, compressor.size) + + t1 = "hello world" + t2 = "" + t3 = "bingo" + + compressor << t1 + assert_equals(compressor.size, t1.size) + + compressor << t2 + assert_equals(compressor.size, t1.size + t2.size) + + compressor << t3 + assert_equals(compressor.size, t1.size + t2.size + t3.size) + } + end + + def test_crc + runCrcTest(PassThruCompressor) + end +end + +class DeflaterTest < RUNIT::TestCase + include CrcTest + + def test_outputOperator + txt = loadFile("ziptest.rb") + deflate(txt, "deflatertest.bin") + inflatedTxt = inflate("deflatertest.bin") + assert_equals(txt, inflatedTxt) + end + + private + def loadFile(fileName) + txt = nil + File.open(fileName, "rb") { |f| txt = f.read } + end + + def deflate(data, fileName) + File.open(fileName, "wb") { + |file| + deflater = Deflater.new(file) + deflater << data + deflater.finish + assert_equals(deflater.size, data.size) + file << "trailing data for zlib with -MAX_WBITS" + } + end + + def inflate(fileName) + txt = nil + File.open(fileName, "rb") { + |file| + inflater = Inflater.new(file) + txt = inflater.read + } + end + + def test_crc + runCrcTest(Deflater) + end +end + +class ZipOutputStreamTest < RUNIT::TestCase + include AssertEntry + + TEST_ZIP = TestZipFile::TEST_ZIP2.clone + TEST_ZIP.zipName = "output.zip" + + def test_new + zos = ZipOutputStream.new(TEST_ZIP.zipName) + zos.comment = TEST_ZIP.comment + writeTestZip(zos) + zos.close + assertTestZipContents(TEST_ZIP) + end + + def test_open + ZipOutputStream.open(TEST_ZIP.zipName) { + |zos| + zos.comment = TEST_ZIP.comment + writeTestZip(zos) + } + assertTestZipContents(TEST_ZIP) + end + + def test_writingToClosedStream + assertIOErrorInClosedStream { |zos| zos << "hello world" } + assertIOErrorInClosedStream { |zos| zos.puts "hello world" } + assertIOErrorInClosedStream { |zos| zos.write "hello world" } + end + + def test_cannotOpenFile + name = TestFiles::EMPTY_TEST_DIR + begin + zos = ZipOutputStream.open(name) + rescue Exception + assert($!.kind_of?(Errno::EISDIR) || # Linux + $!.kind_of?(Errno::EEXIST) || # Windows/cygwin + $!.kind_of?(Errno::EACCES), # Windows + "Expected Errno::EISDIR (or on win/cygwin: Errno::EEXIST), but was: #{$!.type}") + end + end + + def assertIOErrorInClosedStream + assert_exception(IOError) { + zos = ZipOutputStream.new("test_putOnClosedStream.zip") + zos.close + yield zos + } + end + + def writeTestZip(zos) + TEST_ZIP.entryNames.each { + |entryName| + zos.putNextEntry(entryName) + File.open(entryName, "rb") { |f| zos.write(f.read) } + } + end +end + + + +module Enumerable + def compareEnumerables(otherEnumerable) + otherAsArray = otherEnumerable.to_a + index=0 + each_with_index { + |element, index| + return false unless yield(element, otherAsArray[index]) + } + return index+1 == otherAsArray.size + end +end + + +class ZipCentralDirectoryEntryTest < RUNIT::TestCase + + def test_readFromStream + File.open("testDirectory.bin", "rb") { + |file| + entry = ZipEntry.readCDirEntry(file) + + assert_equals("longAscii.txt", entry.name) + assert_equals(ZipEntry::DEFLATED, entry.compressionMethod) + assert_equals(106490, entry.size) + assert_equals(3784, entry.compressedSize) + assert_equals(0xfcd1799c, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.readCDirEntry(file) + assert_equals("empty.txt", entry.name) + assert_equals(ZipEntry::STORED, entry.compressionMethod) + assert_equals(0, entry.size) + assert_equals(0, entry.compressedSize) + assert_equals(0x0, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.readCDirEntry(file) + assert_equals("short.txt", entry.name) + assert_equals(ZipEntry::STORED, entry.compressionMethod) + assert_equals(6, entry.size) + assert_equals(6, entry.compressedSize) + assert_equals(0xbb76fe69, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.readCDirEntry(file) + assert_equals("longBinary.bin", entry.name) + assert_equals(ZipEntry::DEFLATED, entry.compressionMethod) + assert_equals(1000024, entry.size) + assert_equals(70847, entry.compressedSize) + assert_equals(0x10da7d59, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.readCDirEntry(file) + assert_equals(nil, entry) +# Fields that are not check by this test: +# version made by 2 bytes +# version needed to extract 2 bytes +# general purpose bit flag 2 bytes +# last mod file time 2 bytes +# last mod file date 2 bytes +# compressed size 4 bytes +# uncompressed size 4 bytes +# disk number start 2 bytes +# internal file attributes 2 bytes +# external file attributes 4 bytes +# relative offset of local header 4 bytes + +# file name (variable size) +# extra field (variable size) +# file comment (variable size) + + } + end + + def test_ReadEntryFromTruncatedZipFile + fragment="" + File.open("testDirectory.bin") { |f| fragment = f.read(12) } # cdir entry header is at least 46 bytes + fragment.extend(IOizeString) + entry = ZipEntry.new + entry.readCDirEntry(fragment) + fail "ZipError expected" + rescue ZipError + end + +end + +class ZipCentralDirectoryTest < RUNIT::TestCase + + def test_readFromStream + File.open(TestZipFile::TEST_ZIP2.zipName, "rb") { + |zipFile| + cdir = ZipCentralDirectory.readFromStream(zipFile) + + assert_equals(TestZipFile::TEST_ZIP2.entryNames.size, cdir.size) + assert(cdir.compareEnumerables(TestZipFile::TEST_ZIP2.entryNames) { + |cdirEntry, testEntryName| + cdirEntry.name == testEntryName + }) + assert_equals(TestZipFile::TEST_ZIP2.comment, cdir.comment) + } + end + + def test_readFromInvalidStream + File.open("ziptest.rb", "rb") { + |zipFile| + cdir = ZipCentralDirectory.new + cdir.readFromStream(zipFile) + } + fail "ZipError expected!" + rescue ZipError + end + + def test_ReadFromTruncatedZipFile + fragment="" + File.open("testDirectory.bin") { |f| fragment = f.read } + fragment.slice!(12) # removed part of first cdir entry. eocd structure still complete + fragment.extend(IOizeString) + entry = ZipCentralDirectory.new + entry.readFromStream(fragment) + fail "ZipError expected" + rescue ZipError + end + + def test_writeToStream + entries = [ ZipEntry.new("file.zip", "flimse", "myComment", "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt", "Has a comment too") ] + cdir = ZipCentralDirectory.new(entries, "my zip comment") + File.open("cdirtest.bin", "wb") { |f| cdir.writeToStream(f) } + cdirReadback = ZipCentralDirectory.new + File.open("cdirtest.bin", "rb") { |f| cdirReadback.readFromStream(f) } + + assert_equals(cdir.entries, cdirReadback.entries) + end + + def test_equality + cdir1 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "my zip comment") + cdir2 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "my zip comment") + cdir3 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "comment?") + cdir4 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "comment?") + assert_equals(cdir1, cdir1) + assert_equals(cdir1, cdir2) + + assert(cdir1 != cdir3) + assert(cdir2 != cdir3) + assert(cdir2 != cdir3) + assert(cdir3 != cdir4) + + assert(cdir3 != "hello") + end +end + + +class BasicZipFileTest < RUNIT::TestCase + include AssertEntry + + def setup + @zipFile = ZipFile.new(TestZipFile::TEST_ZIP2.zipName) + @testEntryNameIndex=0 + end + + def nextTestEntryName + retVal=TestZipFile::TEST_ZIP2.entryNames[@testEntryNameIndex] + @testEntryNameIndex+=1 + return retVal + end + + def test_entries + assert_equals(TestZipFile::TEST_ZIP2.entryNames, @zipFile.entries.map {|e| e.name} ) + end + + def test_each + @zipFile.each { + |entry| + assert_equals(nextTestEntryName, entry.name) + } + assert_equals(4, @testEntryNameIndex) + end + + def test_foreach + ZipFile.foreach(TestZipFile::TEST_ZIP2.zipName) { + |entry| + assert_equals(nextTestEntryName, entry.name) + } + assert_equals(4, @testEntryNameIndex) + end + + def test_getInputStream + @zipFile.each { + |entry| + assertEntry(nextTestEntryName, @zipFile.getInputStream(entry), + entry.name) + } + assert_equals(4, @testEntryNameIndex) + end + + def test_getInputStreamBlock + fileAndEntryName = @zipFile.entries.first.name + @zipFile.getInputStream(fileAndEntryName) { + |zis| + assertEntryContentsForStream(fileAndEntryName, + zis, + fileAndEntryName) + } + end +end + +class CommonZipFileFixture < RUNIT::TestCase + include AssertEntry + + EMPTY_FILENAME = "emptyZipFile.zip" + + TEST_ZIP = TestZipFile::TEST_ZIP2.clone + TEST_ZIP.zipName = "4entry_copy.zip" + + def setup + File.delete(EMPTY_FILENAME) if File.exists?(EMPTY_FILENAME) + File.copy(TestZipFile::TEST_ZIP2.zipName, TEST_ZIP.zipName) + end +end + +class ZipFileTest < CommonZipFileFixture + + def test_createFromScratch + comment = "a short comment" + + zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) + zf.comment = comment + zf.close + + zfRead = ZipFile.new(EMPTY_FILENAME) + assert_equals(comment, zfRead.comment) + assert_equals(0, zfRead.entries.length) + end + + def test_add + srcFile = "ziptest.rb" + entryName = "newEntryName.rb" + assert(File.exists? srcFile) + zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) + zf.add(entryName, srcFile) + zf.close + + zfRead = ZipFile.new(EMPTY_FILENAME) + assert_equals("", zfRead.comment) + assert_equals(1, zfRead.entries.length) + assert_equals(entryName, zfRead.entries.first.name) + AssertEntry.assertContents(srcFile, + zfRead.getInputStream(entryName) { |zis| zis.read }) + end + + def test_addExistingEntryName + assert_exception(ZipEntryExistsError) { + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.add(zf.entries.first.name, "ziptest.rb") + } + } + end + + def test_addExistingEntryNameReplace + gotCalled = false + replacedEntry = nil + ZipFile.open(TEST_ZIP.zipName) { + |zf| + replacedEntry = zf.entries.first.name + zf.add(replacedEntry, "ziptest.rb") { gotCalled = true; true } + } + assert(gotCalled) + ZipFile.open(TEST_ZIP.zipName) { + |zf| + assertContains(zf, replacedEntry, "ziptest.rb") + } + end + + def test_addDirectory + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.add(TestFiles::EMPTY_TEST_DIR, TestFiles::EMPTY_TEST_DIR) + } + ZipFile.open(TEST_ZIP.zipName) { + |zf| + dirEntry = zf.entries.detect { |e| e.name == TestFiles::EMPTY_TEST_DIR+"/" } + assert(dirEntry.isDirectory) + } + end + + def test_remove + entryToRemove, *remainingEntries = TEST_ZIP.entryNames + + File.copy(TestZipFile::TEST_ZIP2.zipName, TEST_ZIP.zipName) + + zf = ZipFile.new(TEST_ZIP.zipName) + assert(zf.entries.map { |e| e.name }.include?(entryToRemove)) + zf.remove(entryToRemove) + assert(! zf.entries.map { |e| e.name }.include?(entryToRemove)) + assert_equals(zf.entries.map {|x| x.name }.sort, remainingEntries.sort) + zf.close + + zfRead = ZipFile.new(TEST_ZIP.zipName) + assert(! zfRead.entries.map { |e| e.name }.include?(entryToRemove)) + assert_equals(zfRead.entries.map {|x| x.name }.sort, remainingEntries.sort) + zfRead.close + end + + + def test_rename + entryToRename, *remainingEntries = TEST_ZIP.entryNames + + zf = ZipFile.new(TEST_ZIP.zipName) + assert(zf.entries.map { |e| e.name }.include? entryToRename) + + newName = "changed name" + assert(! zf.entries.map { |e| e.name }.include?(newName)) + + zf.rename(entryToRename, newName) + assert(zf.entries.map { |e| e.name }.include? newName) + + zf.close + + zfRead = ZipFile.new(TEST_ZIP.zipName) + assert(zfRead.entries.map { |e| e.name }.include? newName) + zfRead.close + end + + def test_renameToExistingEntry + oldEntries = nil + ZipFile.open(TEST_ZIP.zipName) { |zf| oldEntries = zf.entries } + + assert_exception(ZipEntryExistsError) { + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.rename(zf.entries[0], zf.entries[1].name) + } + } + + ZipFile.open(TEST_ZIP.zipName) { + |zf| + assert_equals(oldEntries.map{ |e| e.name }, zf.entries.map{ |e| e.name }) + } + end + + def test_renameToExistingEntryOverwrite + oldEntries = nil + ZipFile.open(TEST_ZIP.zipName) { |zf| oldEntries = zf.entries } + + gotCalled = false + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.rename(zf.entries[0], zf.entries[1].name) { gotCalled = true; true } + } + + assert(gotCalled) + oldEntries.delete_at(0) + ZipFile.open(TEST_ZIP.zipName) { + |zf| + assert_equals(oldEntries.map{ |e| e.name }, + zf.entries.map{ |e| e.name }) + } + end + + def test_renameNonEntry + nonEntry = "bogusEntry" + targetEntry = "targetEntryName" + zf = ZipFile.new(TEST_ZIP.zipName) + assert(! zf.entries.include?(nonEntry)) + assert_exception(ZipNoSuchEntryError) { + zf.rename(nonEntry, targetEntry) + } + zf.commit + assert(! zf.entries.include?(targetEntry)) + ensure + zf.close + end + + def test_renameEntryToExistingEntry + entry1, entry2, *remaining = TEST_ZIP.entryNames + zf = ZipFile.new(TEST_ZIP.zipName) + assert_exception(ZipEntryExistsError) { + zf.rename(entry1, entry2) + } + ensure + zf.close + end + + def test_replace + unchangedEntries = TEST_ZIP.entryNames.dup + entryToReplace = unchangedEntries.delete_at(2) + newEntrySrcFilename = "ziptest.rb" + + zf = ZipFile.new(TEST_ZIP.zipName) + zf.replace(entryToReplace, newEntrySrcFilename) + + zf.close + + zfRead = ZipFile.new(TEST_ZIP.zipName) + AssertEntry::assertContents(newEntrySrcFilename, + zfRead.getInputStream(entryToReplace) { |is| is.read }) + zfRead.close + end + + def test_replaceNonEntry + entryToReplace = "nonExistingEntryname" + ZipFile.open(TEST_ZIP.zipName) { + |zf| + assert_exception(ZipNoSuchEntryError) { + zf.replace(entryToReplace, "ziptest.rb") + } + } + end + + def test_commit + newName = "renamedFirst" + zf = ZipFile.new(TEST_ZIP.zipName) + oldName = zf.entries.first + zf.rename(oldName, newName) + zf.commit + + zfRead = ZipFile.new(TEST_ZIP.zipName) + assert(zfRead.entries.detect { |e| e.name == newName } != nil) + assert(zfRead.entries.detect { |e| e.name == oldName } == nil) + zfRead.close + + zf.close + end + + # This test tests that after commit, you + # can delete the file you used to add the entry to the zip file + # with + def test_commitUseZipEntry + File.copy(TestFiles::RANDOM_ASCII_FILE1, "okToDelete.txt") + zf = ZipFile.open(TEST_ZIP.zipName) + zf.add("okToDelete.txt", "okToDelete.txt") + assertContains(zf, "okToDelete.txt") + zf.commit + File.move("okToDelete.txt", "okToDeleteMoved.txt") + assertContains(zf, "okToDelete.txt", "okToDeleteMoved.txt") + end + +# def test_close +# zf = ZipFile.new(TEST_ZIP.zipName) +# zf.close +# assert_exception(IOError) { +# zf.extract(TEST_ZIP.entryNames.first, "hullubullu") +# } +# end + + def test_compound1 + renamedName = "renamedName" + originalEntries = [] + begin + zf = ZipFile.new(TEST_ZIP.zipName) + originalEntries = zf.entries.dup + + assertNotContains(zf, TestFiles::RANDOM_ASCII_FILE1) + zf.add(TestFiles::RANDOM_ASCII_FILE1, + TestFiles::RANDOM_ASCII_FILE1) + assertContains(zf, TestFiles::RANDOM_ASCII_FILE1) + + zf.rename(zf.entries[0], renamedName) + assertContains(zf, renamedName) + + TestFiles::BINARY_TEST_FILES.each { + |filename| + zf.add(filename, filename) + assertContains(zf, filename) + } + + assertContains(zf, originalEntries.last.to_s) + zf.remove(originalEntries.last.to_s) + assertNotContains(zf, originalEntries.last.to_s) + + ensure + zf.close + end + begin + zfRead = ZipFile.new(TEST_ZIP.zipName) + assertContains(zfRead, TestFiles::RANDOM_ASCII_FILE1) + assertContains(zfRead, renamedName) + TestFiles::BINARY_TEST_FILES.each { + |filename| + assertContains(zfRead, filename) + } + assertNotContains(zfRead, originalEntries.last.to_s) + ensure + zfRead.close + end + end + + def test_compound2 + begin + zf = ZipFile.new(TEST_ZIP.zipName) + originalEntries = zf.entries.dup + + originalEntries.each { + |entry| + zf.remove(entry) + assertNotContains(zf, entry) + } + assert(zf.entries.empty?) + + TestFiles::ASCII_TEST_FILES.each { + |filename| + zf.add(filename, filename) + assertContains(zf, filename) + } + assert_equals(zf.entries.map { |e| e.name }, TestFiles::ASCII_TEST_FILES) + + zf.rename(TestFiles::ASCII_TEST_FILES[0], "newName") + assertNotContains(zf, TestFiles::ASCII_TEST_FILES[0]) + assertContains(zf, "newName") + ensure + zf.close + end + begin + zfRead = ZipFile.new(TEST_ZIP.zipName) + asciiTestFiles = TestFiles::ASCII_TEST_FILES.dup + asciiTestFiles.shift + asciiTestFiles.each { + |filename| + assertContains(zf, filename) + } + + assertContains(zf, "newName") + ensure + zfRead.close + end + end + + private + def assertContains(zf, entryName, filename = entryName) + assert(zf.entries.detect { |e| e.name == entryName} != nil, "entry #{entryName} not in #{zf.entries.join(', ')} in zip file #{zf}") + assertEntryContents(zf, entryName, filename) if File.exists?(filename) + end + + def assertNotContains(zf, entryName) + assert(zf.entries.detect { |e| e.name == entryName} == nil, "entry #{entryName} in #{zf.entries.join(', ')} in zip file #{zf}") + end +end + +class ZipFileExtractTest < CommonZipFileFixture + EXTRACTED_FILENAME = "extEntry" + ENTRY_TO_EXTRACT, *REMAINING_ENTRIES = TEST_ZIP.entryNames.reverse + + def setup + super + File.delete(EXTRACTED_FILENAME) if File.exists?(EXTRACTED_FILENAME) + end + + def test_extract + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.extract(ENTRY_TO_EXTRACT, EXTRACTED_FILENAME) + + assert(File.exists? EXTRACTED_FILENAME) + AssertEntry::assertContents(EXTRACTED_FILENAME, + zf.getInputStream(ENTRY_TO_EXTRACT) { |is| is.read }) + } + end + + def test_extractExists + writtenText = "written text" + File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } + + assert_exception(ZipDestinationFileExistsError) { + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.extract(zf.entries.first, EXTRACTED_FILENAME) + } + } + File.open(EXTRACTED_FILENAME, "r") { + |f| + assert_equals(writtenText, f.read) + } + end + + def test_extractExistsOverwrite + writtenText = "written text" + File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } + + gotCalled = false + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.extract(zf.entries.first, EXTRACTED_FILENAME) { gotCalled = true; true } + } + + assert(gotCalled) + File.open(EXTRACTED_FILENAME, "r") { + |f| + assert(writtenText != f.read) + } + end + + def test_extractNonEntry + zf = ZipFile.new(TEST_ZIP.zipName) + assert_exception(ZipNoSuchEntryError) { zf.extract("nonExistingEntry", "nonExistingEntry") } + ensure + zf.close if zf + end + + def test_extractNonEntry2 + outFile = "outfile" + assert_exception(ZipNoSuchEntryError) { + zf = ZipFile.new(TEST_ZIP.zipName) + nonEntry = "hotdog-diddelidoo" + assert(! zf.entries.include?(nonEntry)) + zf.extract(nonEntry, outFile) + zf.close + } + assert(! File.exists?(outFile)) + end + +end + +class ZipFileExtractDirectoryTest < CommonZipFileFixture + TEST_OUT_NAME = "emptyOutDir" + + def openZip(&aProc) + assert(aProc != nil) + ZipFile.open(TestZipFile::TEST_ZIP4.zipName, &aProc) + end + + def extractTestDir(&aProc) + openZip { + |zf| + zf.extract(TestFiles::EMPTY_TEST_DIR, TEST_OUT_NAME, &aProc) + } + end + + def setup + super + + Dir.rmdir(TEST_OUT_NAME) if File.directory? TEST_OUT_NAME + File.delete(TEST_OUT_NAME) if File.exists? TEST_OUT_NAME + end + + def test_extractDirectory + extractTestDir + assert(File.directory? TEST_OUT_NAME) + end + + def test_extractDirectoryExistsAsDir + Dir.mkdir TEST_OUT_NAME + extractTestDir + assert(File.directory? TEST_OUT_NAME) + end + + def test_extractDirectoryExistsAsFile + File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } + assert_exception(ZipDestinationFileExistsError) { extractTestDir } + end + + def test_extractDirectoryExistsAsFileOverwrite + File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } + gotCalled = false + extractTestDir { + |entry, destPath| + gotCalled = true + assert_equals(TEST_OUT_NAME, destPath) + assert(entry.isDirectory) + true + } + assert(gotCalled) + assert(File.directory? TEST_OUT_NAME) + end +end + + +TestFiles::createTestFiles(ARGV.index("recreate") != nil || + ARGV.index("recreateonly") != nil) +TestZipFile::createTestZips(ARGV.index("recreate") != nil || + ARGV.index("recreateonly") != nil) +exit if ARGV.index("recreateonly") != nil + +#require 'runit/cui/testrunner' +#RUNIT::CUI::TestRunner.run(ZipFileTest.suite) + +# Copyright (C) 2002 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/test/data/notzippedruby.rb b/vendor/plugins/rubyzip-0.9.1/test/data/notzippedruby.rb new file mode 100755 index 00000000..036d25e9 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/test/data/notzippedruby.rb @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +class NotZippedRuby + def returnTrue + true + end +end diff --git a/vendor/plugins/rubyzip-0.9.1/test/data/rubycode.zip b/vendor/plugins/rubyzip-0.9.1/test/data/rubycode.zip new file mode 100644 index 00000000..8a68560e Binary files /dev/null and b/vendor/plugins/rubyzip-0.9.1/test/data/rubycode.zip differ diff --git a/vendor/plugins/rubyzip-0.9.1/test/data/rubycode2.zip b/vendor/plugins/rubyzip-0.9.1/test/data/rubycode2.zip new file mode 100644 index 00000000..8e1cd08f Binary files /dev/null and b/vendor/plugins/rubyzip-0.9.1/test/data/rubycode2.zip differ diff --git a/vendor/plugins/rubyzip-0.9.1/test/data/testDirectory.bin b/vendor/plugins/rubyzip-0.9.1/test/data/testDirectory.bin new file mode 100644 index 00000000..cbdb9f7d Binary files /dev/null and b/vendor/plugins/rubyzip-0.9.1/test/data/testDirectory.bin differ diff --git a/vendor/plugins/rubyzip-0.9.1/test/data/zipWithDirs.zip b/vendor/plugins/rubyzip-0.9.1/test/data/zipWithDirs.zip new file mode 100644 index 00000000..4b01f011 Binary files /dev/null and b/vendor/plugins/rubyzip-0.9.1/test/data/zipWithDirs.zip differ diff --git a/vendor/plugins/rubyzip-0.9.1/test/gentestfiles.rb b/vendor/plugins/rubyzip-0.9.1/test/gentestfiles.rb new file mode 100755 index 00000000..b1b25339 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/test/gentestfiles.rb @@ -0,0 +1,157 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +class TestFiles + RANDOM_ASCII_FILE1 = "data/generated/randomAscii1.txt" + RANDOM_ASCII_FILE2 = "data/generated/randomAscii2.txt" + RANDOM_ASCII_FILE3 = "data/generated/randomAscii3.txt" + RANDOM_BINARY_FILE1 = "data/generated/randomBinary1.bin" + RANDOM_BINARY_FILE2 = "data/generated/randomBinary2.bin" + + EMPTY_TEST_DIR = "data/generated/emptytestdir" + + ASCII_TEST_FILES = [ RANDOM_ASCII_FILE1, RANDOM_ASCII_FILE2, RANDOM_ASCII_FILE3 ] + BINARY_TEST_FILES = [ RANDOM_BINARY_FILE1, RANDOM_BINARY_FILE2 ] + TEST_DIRECTORIES = [ EMPTY_TEST_DIR ] + TEST_FILES = [ ASCII_TEST_FILES, BINARY_TEST_FILES, EMPTY_TEST_DIR ].flatten! + + def TestFiles.create_test_files(recreate) + if (recreate || + ! (TEST_FILES.inject(true) { |accum, element| accum && File.exists?(element) })) + + Dir.mkdir "data/generated" rescue Errno::EEXIST + + ASCII_TEST_FILES.each_with_index { + |filename, index| + create_random_ascii(filename, 1E4 * (index+1)) + } + + BINARY_TEST_FILES.each_with_index { + |filename, index| + create_random_binary(filename, 1E4 * (index+1)) + } + + ensure_dir(EMPTY_TEST_DIR) + end + end + + private + def TestFiles.create_random_ascii(filename, size) + File.open(filename, "wb") { + |file| + while (file.tell < size) + file << rand + end + } + end + + def TestFiles.create_random_binary(filename, size) + File.open(filename, "wb") { + |file| + while (file.tell < size) + file << [rand].pack("V") + end + } + end + + def TestFiles.ensure_dir(name) + if File.exists?(name) + return if File.stat(name).directory? + File.delete(name) + end + Dir.mkdir(name) + end + +end + + + +# For representation and creation of +# test data +class TestZipFile + attr_accessor :zip_name, :entry_names, :comment + + def initialize(zip_name, entry_names, comment = "") + @zip_name=zip_name + @entry_names=entry_names + @comment = comment + end + + def TestZipFile.create_test_zips(recreate) + files = Dir.entries("data/generated") + if (recreate || + ! (files.index(File.basename(TEST_ZIP1.zip_name)) && + files.index(File.basename(TEST_ZIP2.zip_name)) && + files.index(File.basename(TEST_ZIP3.zip_name)) && + files.index(File.basename(TEST_ZIP4.zip_name)) && + files.index("empty.txt") && + files.index("empty_chmod640.txt") && + files.index("short.txt") && + files.index("longAscii.txt") && + files.index("longBinary.bin") )) + raise "failed to create test zip '#{TEST_ZIP1.zip_name}'" unless + system("zip #{TEST_ZIP1.zip_name} data/file2.txt") + raise "failed to remove entry from '#{TEST_ZIP1.zip_name}'" unless + system("zip #{TEST_ZIP1.zip_name} -d data/file2.txt") + + File.open("data/generated/empty.txt", "w") {} + File.open("data/generated/empty_chmod640.txt", "w") { |f| f.chmod(0640) } + + File.open("data/generated/short.txt", "w") { |file| file << "ABCDEF" } + ziptestTxt="" + File.open("data/file2.txt") { |file| ziptestTxt=file.read } + File.open("data/generated/longAscii.txt", "w") { + |file| + while (file.tell < 1E5) + file << ziptestTxt + end + } + + testBinaryPattern="" + File.open("data/generated/empty.zip") { |file| testBinaryPattern=file.read } + testBinaryPattern *= 4 + + File.open("data/generated/longBinary.bin", "wb") { + |file| + while (file.tell < 3E5) + file << testBinaryPattern << rand << "\0" + end + } + raise "failed to create test zip '#{TEST_ZIP2.zip_name}'" unless + system("zip #{TEST_ZIP2.zip_name} #{TEST_ZIP2.entry_names.join(' ')}") + + # without bash system interprets everything after echo as parameters to + # echo including | zip -z ... + raise "failed to add comment to test zip '#{TEST_ZIP2.zip_name}'" unless + system("bash -c \"echo #{TEST_ZIP2.comment} | zip -z #{TEST_ZIP2.zip_name}\"") + + raise "failed to create test zip '#{TEST_ZIP3.zip_name}'" unless + system("zip #{TEST_ZIP3.zip_name} #{TEST_ZIP3.entry_names.join(' ')}") + + raise "failed to create test zip '#{TEST_ZIP4.zip_name}'" unless + system("zip #{TEST_ZIP4.zip_name} #{TEST_ZIP4.entry_names.join(' ')}") + end + rescue + raise $!.to_s + + "\n\nziptest.rb requires the Info-ZIP program 'zip' in the path\n" + + "to create test data. If you don't have it you can download\n" + + "the necessary test files at http://sf.net/projects/rubyzip." + end + + TEST_ZIP1 = TestZipFile.new("data/generated/empty.zip", []) + TEST_ZIP2 = TestZipFile.new("data/generated/5entry.zip", %w{ data/generated/longAscii.txt data/generated/empty.txt data/generated/empty_chmod640.txt data/generated/short.txt data/generated/longBinary.bin}, + "my zip comment") + TEST_ZIP3 = TestZipFile.new("data/generated/test1.zip", %w{ data/file1.txt }) + TEST_ZIP4 = TestZipFile.new("data/generated/zipWithDir.zip", [ "data/file1.txt", + TestFiles::EMPTY_TEST_DIR]) +end + + +END { + TestFiles::create_test_files(ARGV.index("recreate") != nil || + ARGV.index("recreateonly") != nil) + TestZipFile::create_test_zips(ARGV.index("recreate") != nil || + ARGV.index("recreateonly") != nil) + exit if ARGV.index("recreateonly") != nil +} diff --git a/vendor/plugins/rubyzip-0.9.1/test/ioextrastest.rb b/vendor/plugins/rubyzip-0.9.1/test/ioextrastest.rb new file mode 100755 index 00000000..b18e9db9 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/test/ioextrastest.rb @@ -0,0 +1,208 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +$: << "../lib" + +require 'test/unit' +require 'zip/ioextras' + +include IOExtras + +class FakeIOTest < Test::Unit::TestCase + class FakeIOUsingClass + include FakeIO + end + + def test_kind_of? + obj = FakeIOUsingClass.new + + assert(obj.kind_of?(Object)) + assert(obj.kind_of?(FakeIOUsingClass)) + assert(obj.kind_of?(IO)) + assert(!obj.kind_of?(Fixnum)) + assert(!obj.kind_of?(String)) + end +end + +class AbstractInputStreamTest < Test::Unit::TestCase + # AbstractInputStream subclass that provides a read method + + TEST_LINES = [ "Hello world#{$/}", + "this is the second line#{$/}", + "this is the last line"] + TEST_STRING = TEST_LINES.join + class TestAbstractInputStream + include AbstractInputStream + def initialize(aString) + super() + @contents = aString + @readPointer = 0 + end + + def read(charsToRead) + retVal=@contents[@readPointer, charsToRead] + @readPointer+=charsToRead + return retVal + end + + def produce_input + read(100) + end + + def input_finished? + @contents[@readPointer] == nil + end + end + + def setup + @io = TestAbstractInputStream.new(TEST_STRING) + end + + def test_gets + assert_equal(TEST_LINES[0], @io.gets) + assert_equal(1, @io.lineno) + assert_equal(TEST_LINES[1], @io.gets) + assert_equal(2, @io.lineno) + assert_equal(TEST_LINES[2], @io.gets) + assert_equal(3, @io.lineno) + assert_equal(nil, @io.gets) + assert_equal(4, @io.lineno) + end + + def test_getsMultiCharSeperator + assert_equal("Hell", @io.gets("ll")) + assert_equal("o world#{$/}this is the second l", @io.gets("d l")) + end + + def test_each_line + lineNumber=0 + @io.each_line { + |line| + assert_equal(TEST_LINES[lineNumber], line) + lineNumber+=1 + } + end + + def test_readlines + assert_equal(TEST_LINES, @io.readlines) + end + + def test_readline + test_gets + begin + @io.readline + fail "EOFError expected" + rescue EOFError + end + end +end + +class AbstractOutputStreamTest < Test::Unit::TestCase + class TestOutputStream + include AbstractOutputStream + + attr_accessor :buffer + + def initialize + @buffer = "" + end + + def << (data) + @buffer << data + self + end + end + + def setup + @outputStream = TestOutputStream.new + + @origCommaSep = $, + @origOutputSep = $\ + end + + def teardown + $, = @origCommaSep + $\ = @origOutputSep + end + + def test_write + count = @outputStream.write("a little string") + assert_equal("a little string", @outputStream.buffer) + assert_equal("a little string".length, count) + + count = @outputStream.write(". a little more") + assert_equal("a little string. a little more", @outputStream.buffer) + assert_equal(". a little more".length, count) + end + + def test_print + $\ = nil # record separator set to nil + @outputStream.print("hello") + assert_equal("hello", @outputStream.buffer) + + @outputStream.print(" world.") + assert_equal("hello world.", @outputStream.buffer) + + @outputStream.print(" You ok ", "out ", "there?") + assert_equal("hello world. You ok out there?", @outputStream.buffer) + + $\ = "\n" + @outputStream.print + assert_equal("hello world. You ok out there?\n", @outputStream.buffer) + + @outputStream.print("I sure hope so!") + assert_equal("hello world. You ok out there?\nI sure hope so!\n", @outputStream.buffer) + + $, = "X" + @outputStream.buffer = "" + @outputStream.print("monkey", "duck", "zebra") + assert_equal("monkeyXduckXzebra\n", @outputStream.buffer) + + $\ = nil + @outputStream.buffer = "" + @outputStream.print(20) + assert_equal("20", @outputStream.buffer) + end + + def test_printf + @outputStream.printf("%d %04x", 123, 123) + assert_equal("123 007b", @outputStream.buffer) + end + + def test_putc + @outputStream.putc("A") + assert_equal("A", @outputStream.buffer) + @outputStream.putc(65) + assert_equal("AA", @outputStream.buffer) + end + + def test_puts + @outputStream.puts + assert_equal("\n", @outputStream.buffer) + + @outputStream.puts("hello", "world") + assert_equal("\nhello\nworld\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts("hello\n", "world\n") + assert_equal("hello\nworld\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts(["hello\n", "world\n"]) + assert_equal("hello\nworld\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts(["hello\n", "world\n"], "bingo") + assert_equal("hello\nworld\nbingo\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts(16, 20, 50, "hello") + assert_equal("16\n20\n50\nhello\n", @outputStream.buffer) + end +end + + +# Copyright (C) 2002-2004 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/test/stdrubyexttest.rb b/vendor/plugins/rubyzip-0.9.1/test/stdrubyexttest.rb new file mode 100755 index 00000000..f11608f7 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/test/stdrubyexttest.rb @@ -0,0 +1,52 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +$: << "../lib" + +require 'test/unit' +require 'zip/stdrubyext' + +class ModuleTest < Test::Unit::TestCase + + def test_select_map + assert_equal([2, 4, 8, 10], [1, 2, 3, 4, 5].select_map { |e| e == 3 ? nil : 2*e }) + end + +end + +class StringExtensionsTest < Test::Unit::TestCase + + def test_starts_with + assert("hello".starts_with("")) + assert("hello".starts_with("h")) + assert("hello".starts_with("he")) + assert(! "hello".starts_with("hello there")) + assert(! "hello".starts_with(" he")) + + assert_raise(TypeError, "type mismatch: NilClass given") { + "hello".starts_with(nil) + } + end + + def test_ends_with + assert("hello".ends_with("o")) + assert("hello".ends_with("lo")) + assert("hello".ends_with("hello")) + assert(!"howdy".ends_with("o")) + assert(!"howdy".ends_with("oy")) + assert(!"howdy".ends_with("howdy doody")) + assert(!"howdy".ends_with("doody howdy")) + end + + def test_ensure_end + assert_equal("hello!", "hello!".ensure_end("!")) + assert_equal("hello!", "hello!".ensure_end("o!")) + assert_equal("hello!", "hello".ensure_end("!")) + assert_equal("hello!", "hel".ensure_end("lo!")) + end +end + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/test/zipfilesystemtest.rb b/vendor/plugins/rubyzip-0.9.1/test/zipfilesystemtest.rb new file mode 100755 index 00000000..f17b2d80 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/test/zipfilesystemtest.rb @@ -0,0 +1,831 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +$: << "../lib" + +require 'zip/zipfilesystem' +require 'test/unit' + +module ExtraAssertions + + def assert_forwarded(anObject, method, retVal, *expectedArgs) + callArgs = nil + setCallArgsProc = proc { |args| callArgs = args } + anObject.instance_eval <<-"end_eval" + alias #{method}_org #{method} + def #{method}(*args) + ObjectSpace._id2ref(#{setCallArgsProc.object_id}).call(args) + ObjectSpace._id2ref(#{retVal.object_id}) + end + end_eval + + assert_equal(retVal, yield) # Invoke test + assert_equal(expectedArgs, callArgs) + ensure + anObject.instance_eval "alias #{method} #{method}_org" + end + +end + +include Zip + +class ZipFsFileNonmutatingTest < Test::Unit::TestCase + def setup + @zipFile = ZipFile.new("data/zipWithDirs.zip") + end + + def teardown + @zipFile.close if @zipFile + end + + def test_umask + assert_equal(File.umask, @zipFile.file.umask) + @zipFile.file.umask(0006) + end + + def test_exists? + assert(! @zipFile.file.exists?("notAFile")) + assert(@zipFile.file.exists?("file1")) + assert(@zipFile.file.exists?("dir1")) + assert(@zipFile.file.exists?("dir1/")) + assert(@zipFile.file.exists?("dir1/file12")) + assert(@zipFile.file.exist?("dir1/file12")) # notice, tests exist? alias of exists? ! + + @zipFile.dir.chdir "dir1/" + assert(!@zipFile.file.exists?("file1")) + assert(@zipFile.file.exists?("file12")) + end + + def test_open_read + blockCalled = false + @zipFile.file.open("file1", "r") { + |f| + blockCalled = true + assert_equal("this is the entry 'file1' in my test archive!", + f.readline.chomp) + } + assert(blockCalled) + + blockCalled = false + @zipFile.dir.chdir "dir2" + @zipFile.file.open("file21", "r") { + |f| + blockCalled = true + assert_equal("this is the entry 'dir2/file21' in my test archive!", + f.readline.chomp) + } + assert(blockCalled) + @zipFile.dir.chdir "/" + + assert_raise(Errno::ENOENT) { + @zipFile.file.open("noSuchEntry") + } + + begin + is = @zipFile.file.open("file1") + assert_equal("this is the entry 'file1' in my test archive!", + is.readline.chomp) + ensure + is.close if is + end + end + + def test_new + begin + is = @zipFile.file.new("file1") + assert_equal("this is the entry 'file1' in my test archive!", + is.readline.chomp) + ensure + is.close if is + end + begin + is = @zipFile.file.new("file1") { + fail "should not call block" + } + ensure + is.close if is + end + end + + def test_symlink + assert_raise(NotImplementedError) { + @zipFile.file.symlink("file1", "aSymlink") + } + end + + def test_size + assert_raise(Errno::ENOENT) { @zipFile.file.size("notAFile") } + assert_equal(72, @zipFile.file.size("file1")) + assert_equal(0, @zipFile.file.size("dir2/dir21")) + + assert_equal(72, @zipFile.file.stat("file1").size) + assert_equal(0, @zipFile.file.stat("dir2/dir21").size) + end + + def test_size? + assert_equal(nil, @zipFile.file.size?("notAFile")) + assert_equal(72, @zipFile.file.size?("file1")) + assert_equal(nil, @zipFile.file.size?("dir2/dir21")) + + assert_equal(72, @zipFile.file.stat("file1").size?) + assert_equal(nil, @zipFile.file.stat("dir2/dir21").size?) + end + + + def test_file? + assert(@zipFile.file.file?("file1")) + assert(@zipFile.file.file?("dir2/file21")) + assert(! @zipFile.file.file?("dir1")) + assert(! @zipFile.file.file?("dir1/dir11")) + + assert(@zipFile.file.stat("file1").file?) + assert(@zipFile.file.stat("dir2/file21").file?) + assert(! @zipFile.file.stat("dir1").file?) + assert(! @zipFile.file.stat("dir1/dir11").file?) + end + + include ExtraAssertions + + def test_dirname + assert_forwarded(File, :dirname, "retVal", "a/b/c/d") { + @zipFile.file.dirname("a/b/c/d") + } + end + + def test_basename + assert_forwarded(File, :basename, "retVal", "a/b/c/d") { + @zipFile.file.basename("a/b/c/d") + } + end + + def test_split + assert_forwarded(File, :split, "retVal", "a/b/c/d") { + @zipFile.file.split("a/b/c/d") + } + end + + def test_join + assert_equal("a/b/c", @zipFile.file.join("a/b", "c")) + assert_equal("a/b/c/d", @zipFile.file.join("a/b", "c/d")) + assert_equal("/c/d", @zipFile.file.join("", "c/d")) + assert_equal("a/b/c/d", @zipFile.file.join("a", "b", "c", "d")) + end + + def test_utime + t_now = Time.now + t_bak = @zipFile.file.mtime("file1") + @zipFile.file.utime(t_now, "file1") + assert_equal(t_now, @zipFile.file.mtime("file1")) + @zipFile.file.utime(t_bak, "file1") + assert_equal(t_bak, @zipFile.file.mtime("file1")) + end + + + def assert_always_false(operation) + assert(! @zipFile.file.send(operation, "noSuchFile")) + assert(! @zipFile.file.send(operation, "file1")) + assert(! @zipFile.file.send(operation, "dir1")) + assert(! @zipFile.file.stat("file1").send(operation)) + assert(! @zipFile.file.stat("dir1").send(operation)) + end + + def assert_true_if_entry_exists(operation) + assert(! @zipFile.file.send(operation, "noSuchFile")) + assert(@zipFile.file.send(operation, "file1")) + assert(@zipFile.file.send(operation, "dir1")) + assert(@zipFile.file.stat("file1").send(operation)) + assert(@zipFile.file.stat("dir1").send(operation)) + end + + def test_pipe? + assert_always_false(:pipe?) + end + + def test_blockdev? + assert_always_false(:blockdev?) + end + + def test_symlink? + assert_always_false(:symlink?) + end + + def test_socket? + assert_always_false(:socket?) + end + + def test_chardev? + assert_always_false(:chardev?) + end + + def test_truncate + assert_raise(StandardError, "truncate not supported") { + @zipFile.file.truncate("file1", 100) + } + end + + def assert_e_n_o_e_n_t(operation, args = ["NoSuchFile"]) + assert_raise(Errno::ENOENT) { + @zipFile.file.send(operation, *args) + } + end + + def test_ftype + assert_e_n_o_e_n_t(:ftype) + assert_equal("file", @zipFile.file.ftype("file1")) + assert_equal("directory", @zipFile.file.ftype("dir1/dir11")) + assert_equal("directory", @zipFile.file.ftype("dir1/dir11/")) + end + + def test_link + assert_raise(NotImplementedError) { + @zipFile.file.link("file1", "someOtherString") + } + end + + def test_directory? + assert(! @zipFile.file.directory?("notAFile")) + assert(! @zipFile.file.directory?("file1")) + assert(! @zipFile.file.directory?("dir1/file11")) + assert(@zipFile.file.directory?("dir1")) + assert(@zipFile.file.directory?("dir1/")) + assert(@zipFile.file.directory?("dir2/dir21")) + + assert(! @zipFile.file.stat("file1").directory?) + assert(! @zipFile.file.stat("dir1/file11").directory?) + assert(@zipFile.file.stat("dir1").directory?) + assert(@zipFile.file.stat("dir1/").directory?) + assert(@zipFile.file.stat("dir2/dir21").directory?) + end + + def test_chown + assert_equal(2, @zipFile.file.chown(1,2, "dir1", "file1")) + assert_equal(1, @zipFile.file.stat("dir1").uid) + assert_equal(2, @zipFile.file.stat("dir1").gid) + assert_equal(2, @zipFile.file.chown(nil, nil, "dir1", "file1")) + end + + def test_zero? + assert(! @zipFile.file.zero?("notAFile")) + assert(! @zipFile.file.zero?("file1")) + assert(@zipFile.file.zero?("dir1")) + blockCalled = false + ZipFile.open("data/generated/5entry.zip") { + |zf| + blockCalled = true + assert(zf.file.zero?("data/generated/empty.txt")) + } + assert(blockCalled) + + assert(! @zipFile.file.stat("file1").zero?) + assert(@zipFile.file.stat("dir1").zero?) + blockCalled = false + ZipFile.open("data/generated/5entry.zip") { + |zf| + blockCalled = true + assert(zf.file.stat("data/generated/empty.txt").zero?) + } + assert(blockCalled) + end + + def test_expand_path + ZipFile.open("data/zipWithDirs.zip") { + |zf| + assert_equal("/", zf.file.expand_path(".")) + zf.dir.chdir "dir1" + assert_equal("/dir1", zf.file.expand_path(".")) + assert_equal("/dir1/file12", zf.file.expand_path("file12")) + assert_equal("/", zf.file.expand_path("..")) + assert_equal("/dir2/dir21", zf.file.expand_path("../dir2/dir21")) + } + end + + def test_mtime + assert_equal(Time.at(1027694306), + @zipFile.file.mtime("dir2/file21")) + assert_equal(Time.at(1027690863), + @zipFile.file.mtime("dir2/dir21")) + assert_raise(Errno::ENOENT) { + @zipFile.file.mtime("noSuchEntry") + } + + assert_equal(Time.at(1027694306), + @zipFile.file.stat("dir2/file21").mtime) + assert_equal(Time.at(1027690863), + @zipFile.file.stat("dir2/dir21").mtime) + end + + def test_ctime + assert_nil(@zipFile.file.ctime("file1")) + assert_nil(@zipFile.file.stat("file1").ctime) + end + + def test_atime + assert_nil(@zipFile.file.atime("file1")) + assert_nil(@zipFile.file.stat("file1").atime) + end + + def test_readable? + assert(! @zipFile.file.readable?("noSuchFile")) + assert(@zipFile.file.readable?("file1")) + assert(@zipFile.file.readable?("dir1")) + assert(@zipFile.file.stat("file1").readable?) + assert(@zipFile.file.stat("dir1").readable?) + end + + def test_readable_real? + assert(! @zipFile.file.readable_real?("noSuchFile")) + assert(@zipFile.file.readable_real?("file1")) + assert(@zipFile.file.readable_real?("dir1")) + assert(@zipFile.file.stat("file1").readable_real?) + assert(@zipFile.file.stat("dir1").readable_real?) + end + + def test_writable? + assert(! @zipFile.file.writable?("noSuchFile")) + assert(@zipFile.file.writable?("file1")) + assert(@zipFile.file.writable?("dir1")) + assert(@zipFile.file.stat("file1").writable?) + assert(@zipFile.file.stat("dir1").writable?) + end + + def test_writable_real? + assert(! @zipFile.file.writable_real?("noSuchFile")) + assert(@zipFile.file.writable_real?("file1")) + assert(@zipFile.file.writable_real?("dir1")) + assert(@zipFile.file.stat("file1").writable_real?) + assert(@zipFile.file.stat("dir1").writable_real?) + end + + def test_executable? + assert(! @zipFile.file.executable?("noSuchFile")) + assert(! @zipFile.file.executable?("file1")) + assert(@zipFile.file.executable?("dir1")) + assert(! @zipFile.file.stat("file1").executable?) + assert(@zipFile.file.stat("dir1").executable?) + end + + def test_executable_real? + assert(! @zipFile.file.executable_real?("noSuchFile")) + assert(! @zipFile.file.executable_real?("file1")) + assert(@zipFile.file.executable_real?("dir1")) + assert(! @zipFile.file.stat("file1").executable_real?) + assert(@zipFile.file.stat("dir1").executable_real?) + end + + def test_owned? + assert_true_if_entry_exists(:owned?) + end + + def test_grpowned? + assert_true_if_entry_exists(:grpowned?) + end + + def test_setgid? + assert_always_false(:setgid?) + end + + def test_setuid? + assert_always_false(:setgid?) + end + + def test_sticky? + assert_always_false(:sticky?) + end + + def test_readlink + assert_raise(NotImplementedError) { + @zipFile.file.readlink("someString") + } + end + + def test_stat + s = @zipFile.file.stat("file1") + assert(s.kind_of?(File::Stat)) # It pretends + assert_raise(Errno::ENOENT, "No such file or directory - noSuchFile") { + @zipFile.file.stat("noSuchFile") + } + end + + def test_lstat + assert(@zipFile.file.lstat("file1").file?) + end + + + def test_chmod + assert_raise(Errno::ENOENT, "No such file or directory - noSuchFile") { + @zipFile.file.chmod(0644, "file1", "NoSuchFile") + } + assert_equal(2, @zipFile.file.chmod(0644, "file1", "dir1")) + end + + def test_pipe + assert_raise(NotImplementedError) { + @zipFile.file.pipe + } + end + + def test_foreach + ZipFile.open("data/generated/zipWithDir.zip") { + |zf| + ref = [] + File.foreach("data/file1.txt") { |e| ref << e } + + index = 0 + zf.file.foreach("data/file1.txt") { + |l| + assert_equal(ref[index], l) + index = index.next + } + assert_equal(ref.size, index) + } + + ZipFile.open("data/generated/zipWithDir.zip") { + |zf| + ref = [] + File.foreach("data/file1.txt", " ") { |e| ref << e } + + index = 0 + zf.file.foreach("data/file1.txt", " ") { + |l| + assert_equal(ref[index], l) + index = index.next + } + assert_equal(ref.size, index) + } + end + + def test_popen + cmd = /mswin/i =~ RUBY_PLATFORM ? 'dir' : 'ls' + + assert_equal(File.popen(cmd) { |f| f.read }, + @zipFile.file.popen(cmd) { |f| f.read }) + end + +# Can be added later +# def test_select +# fail "implement test" +# end + + def test_readlines + ZipFile.open("data/generated/zipWithDir.zip") { + |zf| + assert_equal(File.readlines("data/file1.txt"), + zf.file.readlines("data/file1.txt")) + } + end + + def test_read + ZipFile.open("data/generated/zipWithDir.zip") { + |zf| + assert_equal(File.read("data/file1.txt"), + zf.file.read("data/file1.txt")) + } + end + +end + +class ZipFsFileStatTest < Test::Unit::TestCase + + def setup + @zipFile = ZipFile.new("data/zipWithDirs.zip") + end + + def teardown + @zipFile.close if @zipFile + end + + def test_blocks + assert_equal(nil, @zipFile.file.stat("file1").blocks) + end + + def test_ino + assert_equal(0, @zipFile.file.stat("file1").ino) + end + + def test_uid + assert_equal(0, @zipFile.file.stat("file1").uid) + end + + def test_gid + assert_equal(0, @zipFile.file.stat("file1").gid) + end + + def test_ftype + assert_equal("file", @zipFile.file.stat("file1").ftype) + assert_equal("directory", @zipFile.file.stat("dir1").ftype) + end + + def test_mode + assert_equal(0600, @zipFile.file.stat("file1").mode & 0777) + assert_equal(0600, @zipFile.file.stat("file1").mode & 0777) + assert_equal(0755, @zipFile.file.stat("dir1").mode & 0777) + assert_equal(0755, @zipFile.file.stat("dir1").mode & 0777) + end + + def test_dev + assert_equal(0, @zipFile.file.stat("file1").dev) + end + + def test_rdev + assert_equal(0, @zipFile.file.stat("file1").rdev) + end + + def test_rdev_major + assert_equal(0, @zipFile.file.stat("file1").rdev_major) + end + + def test_rdev_minor + assert_equal(0, @zipFile.file.stat("file1").rdev_minor) + end + + def test_nlink + assert_equal(1, @zipFile.file.stat("file1").nlink) + end + + def test_blksize + assert_nil(@zipFile.file.stat("file1").blksize) + end + +end + +class ZipFsFileMutatingTest < Test::Unit::TestCase + TEST_ZIP = "zipWithDirs_copy.zip" + def setup + File.copy("data/zipWithDirs.zip", TEST_ZIP) + end + + def teardown + end + + def test_delete + do_test_delete_or_unlink(:delete) + end + + def test_unlink + do_test_delete_or_unlink(:unlink) + end + + def test_open_write + ZipFile.open(TEST_ZIP) { + |zf| + + zf.file.open("test_open_write_entry", "w") { + |f| + blockCalled = true + f.write "This is what I'm writing" + } + assert_equal("This is what I'm writing", + zf.file.read("test_open_write_entry")) + + # Test with existing entry + zf.file.open("file1", "w") { + |f| + blockCalled = true + f.write "This is what I'm writing too" + } + assert_equal("This is what I'm writing too", + zf.file.read("file1")) + } + end + + def test_rename + ZipFile.open(TEST_ZIP) { + |zf| + assert_raise(Errno::ENOENT, "") { + zf.file.rename("NoSuchFile", "bimse") + } + zf.file.rename("file1", "newNameForFile1") + } + + ZipFile.open(TEST_ZIP) { + |zf| + assert(! zf.file.exists?("file1")) + assert(zf.file.exists?("newNameForFile1")) + } + end + + def do_test_delete_or_unlink(symbol) + ZipFile.open(TEST_ZIP) { + |zf| + assert(zf.file.exists?("dir2/dir21/dir221/file2221")) + zf.file.send(symbol, "dir2/dir21/dir221/file2221") + assert(! zf.file.exists?("dir2/dir21/dir221/file2221")) + + assert(zf.file.exists?("dir1/file11")) + assert(zf.file.exists?("dir1/file12")) + zf.file.send(symbol, "dir1/file11", "dir1/file12") + assert(! zf.file.exists?("dir1/file11")) + assert(! zf.file.exists?("dir1/file12")) + + assert_raise(Errno::ENOENT) { zf.file.send(symbol, "noSuchFile") } + assert_raise(Errno::EISDIR) { zf.file.send(symbol, "dir1/dir11") } + assert_raise(Errno::EISDIR) { zf.file.send(symbol, "dir1/dir11/") } + } + + ZipFile.open(TEST_ZIP) { + |zf| + assert(! zf.file.exists?("dir2/dir21/dir221/file2221")) + assert(! zf.file.exists?("dir1/file11")) + assert(! zf.file.exists?("dir1/file12")) + + assert(zf.file.exists?("dir1/dir11")) + assert(zf.file.exists?("dir1/dir11/")) + } + end + +end + +class ZipFsDirectoryTest < Test::Unit::TestCase + TEST_ZIP = "zipWithDirs_copy.zip" + + def setup + File.copy("data/zipWithDirs.zip", TEST_ZIP) + end + + def test_delete + ZipFile.open(TEST_ZIP) { + |zf| + assert_raise(Errno::ENOENT, "No such file or directory - NoSuchFile.txt") { + zf.dir.delete("NoSuchFile.txt") + } + assert_raise(Errno::EINVAL, "Invalid argument - file1") { + zf.dir.delete("file1") + } + assert(zf.file.exists?("dir1")) + zf.dir.delete("dir1") + assert(! zf.file.exists?("dir1")) + } + end + + def test_mkdir + ZipFile.open(TEST_ZIP) { + |zf| + assert_raise(Errno::EEXIST, "File exists - dir1") { + zf.dir.mkdir("file1") + } + assert_raise(Errno::EEXIST, "File exists - dir1") { + zf.dir.mkdir("dir1") + } + assert(!zf.file.exists?("newDir")) + zf.dir.mkdir("newDir") + assert(zf.file.directory?("newDir")) + assert(!zf.file.exists?("newDir2")) + zf.dir.mkdir("newDir2", 3485) + assert(zf.file.directory?("newDir2")) + } + end + + def test_pwd_chdir_entries + ZipFile.open(TEST_ZIP) { + |zf| + assert_equal("/", zf.dir.pwd) + + assert_raise(Errno::ENOENT, "No such file or directory - no such dir") { + zf.dir.chdir "no such dir" + } + + assert_raise(Errno::EINVAL, "Invalid argument - file1") { + zf.dir.chdir "file1" + } + + assert_equal(["dir1", "dir2", "file1"].sort, zf.dir.entries(".").sort) + zf.dir.chdir "dir1" + assert_equal("/dir1", zf.dir.pwd) + assert_equal(["dir11", "file11", "file12"], zf.dir.entries(".").sort) + + zf.dir.chdir "../dir2/dir21" + assert_equal("/dir2/dir21", zf.dir.pwd) + assert_equal(["dir221"].sort, zf.dir.entries(".").sort) + } + end + + def test_foreach + ZipFile.open(TEST_ZIP) { + |zf| + + blockCalled = false + assert_raise(Errno::ENOENT, "No such file or directory - noSuchDir") { + zf.dir.foreach("noSuchDir") { |e| blockCalled = true } + } + assert(! blockCalled) + + assert_raise(Errno::ENOTDIR, "Not a directory - file1") { + zf.dir.foreach("file1") { |e| blockCalled = true } + } + assert(! blockCalled) + + entries = [] + zf.dir.foreach(".") { |e| entries << e } + assert_equal(["dir1", "dir2", "file1"].sort, entries.sort) + + entries = [] + zf.dir.foreach("dir1") { |e| entries << e } + assert_equal(["dir11", "file11", "file12"], entries.sort) + } + end + + def test_chroot + ZipFile.open(TEST_ZIP) { + |zf| + assert_raise(NotImplementedError) { + zf.dir.chroot + } + } + end + + # Globbing not supported yet + #def test_glob + # # test alias []-operator too + # fail "implement test" + #end + + def test_open_new + ZipFile.open(TEST_ZIP) { + |zf| + + assert_raise(Errno::ENOTDIR, "Not a directory - file1") { + zf.dir.new("file1") + } + + assert_raise(Errno::ENOENT, "No such file or directory - noSuchFile") { + zf.dir.new("noSuchFile") + } + + d = zf.dir.new(".") + assert_equal(["file1", "dir1", "dir2"].sort, d.entries.sort) + d.close + + zf.dir.open("dir1") { + |d| + assert_equal(["dir11", "file11", "file12"].sort, d.entries.sort) + } + } + end + +end + +class ZipFsDirIteratorTest < Test::Unit::TestCase + + FILENAME_ARRAY = [ "f1", "f2", "f3", "f4", "f5", "f6" ] + + def setup + @dirIt = ZipFileSystem::ZipFsDirIterator.new(FILENAME_ARRAY) + end + + def test_close + @dirIt.close + assert_raise(IOError, "closed directory") { + @dirIt.each { |e| p e } + } + assert_raise(IOError, "closed directory") { + @dirIt.read + } + assert_raise(IOError, "closed directory") { + @dirIt.rewind + } + assert_raise(IOError, "closed directory") { + @dirIt.seek(0) + } + assert_raise(IOError, "closed directory") { + @dirIt.tell + } + + end + + def test_each + # Tested through Enumerable.entries + assert_equal(FILENAME_ARRAY, @dirIt.entries) + end + + def test_read + FILENAME_ARRAY.size.times { + |i| + assert_equal(FILENAME_ARRAY[i], @dirIt.read) + } + end + + def test_rewind + @dirIt.read + @dirIt.read + assert_equal(FILENAME_ARRAY[2], @dirIt.read) + @dirIt.rewind + assert_equal(FILENAME_ARRAY[0], @dirIt.read) + end + + def test_tell_seek + @dirIt.read + @dirIt.read + pos = @dirIt.tell + valAtPos = @dirIt.read + @dirIt.read + @dirIt.seek(pos) + assert_equal(valAtPos, @dirIt.read) + end + +end + + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/test/ziprequiretest.rb b/vendor/plugins/rubyzip-0.9.1/test/ziprequiretest.rb new file mode 100755 index 00000000..68d2c714 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/test/ziprequiretest.rb @@ -0,0 +1,43 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +$: << "../lib" + +require 'test/unit' +require 'zip/ziprequire' + +$: << 'data/rubycode.zip' << 'data/rubycode2.zip' + +class ZipRequireTest < Test::Unit::TestCase + def test_require + assert(require('data/notzippedruby')) + assert(!require('data/notzippedruby')) + + assert(require('zippedruby1')) + assert(!require('zippedruby1')) + + assert(require('zippedruby2')) + assert(!require('zippedruby2')) + + assert(require('zippedruby3')) + assert(!require('zippedruby3')) + + c1 = NotZippedRuby.new + assert(c1.returnTrue) + assert(ZippedRuby1.returnTrue) + assert(!ZippedRuby2.returnFalse) + assert_equal(4, ZippedRuby3.multiplyValues(2, 2)) + end + + def test_get_resource + get_resource("aResource.txt") { + |f| + assert_equal("Nothing exciting in this file!", f.read) + } + end +end + +# Copyright (C) 2002 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/test/ziptest.rb b/vendor/plugins/rubyzip-0.9.1/test/ziptest.rb new file mode 100755 index 00000000..56176446 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/test/ziptest.rb @@ -0,0 +1,1599 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +$: << "../lib" + +require 'test/unit' +require 'zip/zip' +require 'gentestfiles' + +include Zip + + +class ZipEntryTest < Test::Unit::TestCase + TEST_ZIPFILE = "someZipFile.zip" + TEST_COMMENT = "a comment" + TEST_COMPRESSED_SIZE = 1234 + TEST_CRC = 325324 + TEST_EXTRA = "Some data here" + TEST_COMPRESSIONMETHOD = ZipEntry::DEFLATED + TEST_NAME = "entry name" + TEST_SIZE = 8432 + TEST_ISDIRECTORY = false + + def test_constructorAndGetters + entry = ZipEntry.new(TEST_ZIPFILE, + TEST_NAME, + TEST_COMMENT, + TEST_EXTRA, + TEST_COMPRESSED_SIZE, + TEST_CRC, + TEST_COMPRESSIONMETHOD, + TEST_SIZE) + + assert_equal(TEST_COMMENT, entry.comment) + assert_equal(TEST_COMPRESSED_SIZE, entry.compressed_size) + assert_equal(TEST_CRC, entry.crc) + assert_instance_of(Zip::ZipExtraField, entry.extra) + assert_equal(TEST_COMPRESSIONMETHOD, entry.compression_method) + assert_equal(TEST_NAME, entry.name) + assert_equal(TEST_SIZE, entry.size) + assert_equal(TEST_ISDIRECTORY, entry.is_directory) + end + + def test_is_directoryAndIsFile + assert(ZipEntry.new(TEST_ZIPFILE, "hello").file?) + assert(! ZipEntry.new(TEST_ZIPFILE, "hello").directory?) + + assert(ZipEntry.new(TEST_ZIPFILE, "dir/hello").file?) + assert(! ZipEntry.new(TEST_ZIPFILE, "dir/hello").directory?) + + assert(ZipEntry.new(TEST_ZIPFILE, "hello/").directory?) + assert(! ZipEntry.new(TEST_ZIPFILE, "hello/").file?) + + assert(ZipEntry.new(TEST_ZIPFILE, "dir/hello/").directory?) + assert(! ZipEntry.new(TEST_ZIPFILE, "dir/hello/").file?) + end + + def test_equality + entry1 = ZipEntry.new("file.zip", "name", "isNotCompared", + "something extra", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry2 = ZipEntry.new("file.zip", "name", "isNotComparedXXX", + "something extra", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry3 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extra", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry4 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry5 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 1234, + ZipEntry::DEFLATED, 10000) + entry6 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 123, + ZipEntry::DEFLATED, 10000) + entry7 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 123, + ZipEntry::STORED, 10000) + entry8 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 123, + ZipEntry::STORED, 100000) + + assert_equal(entry1, entry1) + assert_equal(entry1, entry2) + + assert(entry2 != entry3) + assert(entry3 != entry4) + assert(entry4 != entry5) + assert(entry5 != entry6) + assert(entry6 != entry7) + assert(entry7 != entry8) + + assert(entry7 != "hello") + assert(entry7 != 12) + end + + def test_compare + assert_equal(0, (ZipEntry.new("zf.zip", "a") <=> ZipEntry.new("zf.zip", "a"))) + assert_equal(1, (ZipEntry.new("zf.zip", "b") <=> ZipEntry.new("zf.zip", "a"))) + assert_equal(-1, (ZipEntry.new("zf.zip", "a") <=> ZipEntry.new("zf.zip", "b"))) + + entries = [ + ZipEntry.new("zf.zip", "5"), + ZipEntry.new("zf.zip", "1"), + ZipEntry.new("zf.zip", "3"), + ZipEntry.new("zf.zip", "4"), + ZipEntry.new("zf.zip", "0"), + ZipEntry.new("zf.zip", "2") + ] + + entries.sort! + assert_equal("0", entries[0].to_s) + assert_equal("1", entries[1].to_s) + assert_equal("2", entries[2].to_s) + assert_equal("3", entries[3].to_s) + assert_equal("4", entries[4].to_s) + assert_equal("5", entries[5].to_s) + end + + def test_parentAsString + entry1 = ZipEntry.new("zf.zip", "aa") + entry2 = ZipEntry.new("zf.zip", "aa/") + entry3 = ZipEntry.new("zf.zip", "aa/bb") + entry4 = ZipEntry.new("zf.zip", "aa/bb/") + entry5 = ZipEntry.new("zf.zip", "aa/bb/cc") + entry6 = ZipEntry.new("zf.zip", "aa/bb/cc/") + + assert_equal(nil, entry1.parent_as_string) + assert_equal(nil, entry2.parent_as_string) + assert_equal("aa/", entry3.parent_as_string) + assert_equal("aa/", entry4.parent_as_string) + assert_equal("aa/bb/", entry5.parent_as_string) + assert_equal("aa/bb/", entry6.parent_as_string) + end + + def test_entry_name_cannot_start_with_slash + assert_raise(ZipEntryNameError) { ZipEntry.new("zf.zip", "/hej/der") } + end +end + +module IOizeString + attr_reader :tell + + def read(count = nil) + @tell ||= 0 + count = size unless count + retVal = slice(@tell, count) + @tell += count + return retVal + end + + def seek(index, offset) + @tell ||= 0 + case offset + when IO::SEEK_END + newPos = size + index + when IO::SEEK_SET + newPos = index + when IO::SEEK_CUR + newPos = @tell + index + else + raise "Error in test method IOizeString::seek" + end + if (newPos < 0 || newPos >= size) + raise Errno::EINVAL + else + @tell=newPos + end + end + + def reset + @tell = 0 + end +end + +class ZipLocalEntryTest < Test::Unit::TestCase + def test_read_local_entryHeaderOfFirstTestZipEntry + File.open(TestZipFile::TEST_ZIP3.zip_name, "rb") { + |file| + entry = ZipEntry.read_local_entry(file) + + assert_equal("", entry.comment) + # Differs from windows and unix because of CR LF + # assert_equal(480, entry.compressed_size) + # assert_equal(0x2a27930f, entry.crc) + # extra field is 21 bytes long + # probably contains some unix attrutes or something + # disabled: assert_equal(nil, entry.extra) + assert_equal(ZipEntry::DEFLATED, entry.compression_method) + assert_equal(TestZipFile::TEST_ZIP3.entry_names[0], entry.name) + assert_equal(File.size(TestZipFile::TEST_ZIP3.entry_names[0]), entry.size) + assert(! entry.is_directory) + } + end + + def test_readDateTime + File.open("data/rubycode.zip", "rb") { + |file| + entry = ZipEntry.read_local_entry(file) + assert_equal("zippedruby1.rb", entry.name) + assert_equal(Time.at(1019261638), entry.time) + } + end + + def test_read_local_entryFromNonZipFile + File.open("data/file2.txt") { + |file| + assert_equal(nil, ZipEntry.read_local_entry(file)) + } + end + + def test_read_local_entryFromTruncatedZipFile + zipFragment="" + File.open(TestZipFile::TEST_ZIP2.zip_name) { |f| zipFragment = f.read(12) } # local header is at least 30 bytes + zipFragment.extend(IOizeString).reset + entry = ZipEntry.new + entry.read_local_entry(zipFragment) + fail "ZipError expected" + rescue ZipError + end + + def test_writeEntry + entry = ZipEntry.new("file.zip", "entryName", "my little comment", + "thisIsSomeExtraInformation", 100, 987654, + ZipEntry::DEFLATED, 400) + write_to_file("localEntryHeader.bin", "centralEntryHeader.bin", entry) + entryReadLocal, entryReadCentral = read_from_file("localEntryHeader.bin", "centralEntryHeader.bin") + compare_local_entry_headers(entry, entryReadLocal) + compare_c_dir_entry_headers(entry, entryReadCentral) + end + + private + def compare_local_entry_headers(entry1, entry2) + assert_equal(entry1.compressed_size , entry2.compressed_size) + assert_equal(entry1.crc , entry2.crc) + assert_equal(entry1.extra , entry2.extra) + assert_equal(entry1.compression_method, entry2.compression_method) + assert_equal(entry1.name , entry2.name) + assert_equal(entry1.size , entry2.size) + assert_equal(entry1.localHeaderOffset, entry2.localHeaderOffset) + end + + def compare_c_dir_entry_headers(entry1, entry2) + compare_local_entry_headers(entry1, entry2) + assert_equal(entry1.comment, entry2.comment) + end + + def write_to_file(localFileName, centralFileName, entry) + File.open(localFileName, "wb") { |f| entry.write_local_entry(f) } + File.open(centralFileName, "wb") { |f| entry.write_c_dir_entry(f) } + end + + def read_from_file(localFileName, centralFileName) + localEntry = nil + cdirEntry = nil + File.open(localFileName, "rb") { |f| localEntry = ZipEntry.read_local_entry(f) } + File.open(centralFileName, "rb") { |f| cdirEntry = ZipEntry.read_c_dir_entry(f) } + return [localEntry, cdirEntry] + end +end + + +module DecompressorTests + # expects @refText, @refLines and @decompressor + + TEST_FILE="data/file1.txt" + + def setup + @refText="" + File.open(TEST_FILE) { |f| @refText = f.read } + @refLines = @refText.split($/) + end + + def test_readEverything + assert_equal(@refText, @decompressor.sysread) + end + + def test_readInChunks + chunkSize = 5 + while (decompressedChunk = @decompressor.sysread(chunkSize)) + assert_equal(@refText.slice!(0, chunkSize), decompressedChunk) + end + assert_equal(0, @refText.size) + end + + def test_mixingReadsAndProduceInput + # Just some preconditions to make sure we have enough data for this test + assert(@refText.length > 1000) + assert(@refLines.length > 40) + + + assert_equal(@refText[0...100], @decompressor.sysread(100)) + + assert(! @decompressor.input_finished?) + buf = @decompressor.produce_input + assert_equal(@refText[100...(100+buf.length)], buf) + end +end + +class InflaterTest < Test::Unit::TestCase + include DecompressorTests + + def setup + super + @file = File.new("data/file1.txt.deflatedData", "rb") + @decompressor = Inflater.new(@file) + end + + def teardown + @file.close + end +end + + +class PassThruDecompressorTest < Test::Unit::TestCase + include DecompressorTests + def setup + super + @file = File.new(TEST_FILE) + @decompressor = PassThruDecompressor.new(@file, File.size(TEST_FILE)) + end + + def teardown + @file.close + end +end + + +module AssertEntry + def assert_next_entry(filename, zis) + assert_entry(filename, zis, zis.get_next_entry.name) + end + + def assert_entry(filename, zis, entryName) + assert_equal(filename, entryName) + assert_entryContentsForStream(filename, zis, entryName) + end + + def assert_entryContentsForStream(filename, zis, entryName) + File.open(filename, "rb") { + |file| + expected = file.read + actual = zis.read + if (expected != actual) + if ((expected && actual) && (expected.length > 400 || actual.length > 400)) + zipEntryFilename=entryName+".zipEntry" + File.open(zipEntryFilename, "wb") { |file| file << actual } + fail("File '#{filename}' is different from '#{zipEntryFilename}'") + else + assert_equal(expected, actual) + end + end + } + end + + def AssertEntry.assert_contents(filename, aString) + fileContents = "" + File.open(filename, "rb") { |f| fileContents = f.read } + if (fileContents != aString) + if (fileContents.length > 400 || aString.length > 400) + stringFile = filename + ".other" + File.open(stringFile, "wb") { |f| f << aString } + fail("File '#{filename}' is different from contents of string stored in '#{stringFile}'") + else + assert_equal(fileContents, aString) + end + end + end + + def assert_stream_contents(zis, testZipFile) + assert(zis != nil) + testZipFile.entry_names.each { + |entryName| + assert_next_entry(entryName, zis) + } + assert_equal(nil, zis.get_next_entry) + end + + def assert_test_zip_contents(testZipFile) + ZipInputStream.open(testZipFile.zip_name) { + |zis| + assert_stream_contents(zis, testZipFile) + } + end + + def assert_entryContents(zipFile, entryName, filename = entryName.to_s) + zis = zipFile.get_input_stream(entryName) + assert_entryContentsForStream(filename, zis, entryName) + ensure + zis.close if zis + end +end + + + +class ZipInputStreamTest < Test::Unit::TestCase + include AssertEntry + + def test_new + zis = ZipInputStream.new(TestZipFile::TEST_ZIP2.zip_name) + assert_stream_contents(zis, TestZipFile::TEST_ZIP2) + assert_equal(true, zis.eof?) + zis.close + end + + def test_openWithBlock + ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) { + |zis| + assert_stream_contents(zis, TestZipFile::TEST_ZIP2) + assert_equal(true, zis.eof?) + } + end + + def test_openWithoutBlock + zis = ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) + assert_stream_contents(zis, TestZipFile::TEST_ZIP2) + end + + def test_incompleteReads + ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) { + |zis| + entry = zis.get_next_entry # longAscii.txt + assert_equal(false, zis.eof?) + assert_equal(TestZipFile::TEST_ZIP2.entry_names[0], entry.name) + assert zis.gets.length > 0 + assert_equal(false, zis.eof?) + entry = zis.get_next_entry # empty.txt + assert_equal(TestZipFile::TEST_ZIP2.entry_names[1], entry.name) + assert_equal(0, entry.size) + assert_equal(nil, zis.gets) + assert_equal(true, zis.eof?) + entry = zis.get_next_entry # empty_chmod640.txt + assert_equal(TestZipFile::TEST_ZIP2.entry_names[2], entry.name) + assert_equal(0, entry.size) + assert_equal(nil, zis.gets) + assert_equal(true, zis.eof?) + entry = zis.get_next_entry # short.txt + assert_equal(TestZipFile::TEST_ZIP2.entry_names[3], entry.name) + assert zis.gets.length > 0 + entry = zis.get_next_entry # longBinary.bin + assert_equal(TestZipFile::TEST_ZIP2.entry_names[4], entry.name) + assert zis.gets.length > 0 + } + end + + def test_rewind + ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) { + |zis| + e = zis.get_next_entry + assert_equal(TestZipFile::TEST_ZIP2.entry_names[0], e.name) + + # Do a little reading + buf = "" + buf << zis.read(100) + buf << (zis.gets || "") + buf << (zis.gets || "") + assert_equal(false, zis.eof?) + + zis.rewind + + buf2 = "" + buf2 << zis.read(100) + buf2 << (zis.gets || "") + buf2 << (zis.gets || "") + + assert_equal(buf, buf2) + + zis.rewind + assert_equal(false, zis.eof?) + + assert_entry(e.name, zis, e.name) + } + end + + def test_mix_read_and_gets + ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) { + |zis| + e = zis.get_next_entry + assert_equal("#!/usr/bin/env ruby", zis.gets.chomp) + assert_equal(false, zis.eof?) + assert_equal("", zis.gets.chomp) + assert_equal(false, zis.eof?) + assert_equal("$VERBOSE =", zis.read(10)) + assert_equal(false, zis.eof?) + } + end + +end + + +module CrcTest + + class TestOutputStream + include IOExtras::AbstractOutputStream + + attr_accessor :buffer + + def initialize + @buffer = "" + end + + def << (data) + @buffer << data + self + end + end + + def run_crc_test(compressorClass) + str = "Here's a nice little text to compute the crc for! Ho hum, it is nice nice nice nice indeed." + fakeOut = TestOutputStream.new + + deflater = compressorClass.new(fakeOut) + deflater << str + assert_equal(0x919920fc, deflater.crc) + end +end + + + +class PassThruCompressorTest < Test::Unit::TestCase + include CrcTest + + def test_size + File.open("dummy.txt", "wb") { + |file| + compressor = PassThruCompressor.new(file) + + assert_equal(0, compressor.size) + + t1 = "hello world" + t2 = "" + t3 = "bingo" + + compressor << t1 + assert_equal(compressor.size, t1.size) + + compressor << t2 + assert_equal(compressor.size, t1.size + t2.size) + + compressor << t3 + assert_equal(compressor.size, t1.size + t2.size + t3.size) + } + end + + def test_crc + run_crc_test(PassThruCompressor) + end +end + +class DeflaterTest < Test::Unit::TestCase + include CrcTest + + def test_outputOperator + txt = load_file("data/file2.txt") + deflate(txt, "deflatertest.bin") + inflatedTxt = inflate("deflatertest.bin") + assert_equal(txt, inflatedTxt) + end + + private + def load_file(fileName) + txt = nil + File.open(fileName, "rb") { |f| txt = f.read } + end + + def deflate(data, fileName) + File.open(fileName, "wb") { + |file| + deflater = Deflater.new(file) + deflater << data + deflater.finish + assert_equal(deflater.size, data.size) + file << "trailing data for zlib with -MAX_WBITS" + } + end + + def inflate(fileName) + txt = nil + File.open(fileName, "rb") { + |file| + inflater = Inflater.new(file) + txt = inflater.sysread + } + end + + def test_crc + run_crc_test(Deflater) + end +end + +class ZipOutputStreamTest < Test::Unit::TestCase + include AssertEntry + + TEST_ZIP = TestZipFile::TEST_ZIP2.clone + TEST_ZIP.zip_name = "output.zip" + + def test_new + zos = ZipOutputStream.new(TEST_ZIP.zip_name) + zos.comment = TEST_ZIP.comment + write_test_zip(zos) + zos.close + assert_test_zip_contents(TEST_ZIP) + end + + def test_open + ZipOutputStream.open(TEST_ZIP.zip_name) { + |zos| + zos.comment = TEST_ZIP.comment + write_test_zip(zos) + } + assert_test_zip_contents(TEST_ZIP) + end + + def test_writingToClosedStream + assert_i_o_error_in_closed_stream { |zos| zos << "hello world" } + assert_i_o_error_in_closed_stream { |zos| zos.puts "hello world" } + assert_i_o_error_in_closed_stream { |zos| zos.write "hello world" } + end + + def test_cannotOpenFile + name = TestFiles::EMPTY_TEST_DIR + begin + zos = ZipOutputStream.open(name) + rescue Exception + assert($!.kind_of?(Errno::EISDIR) || # Linux + $!.kind_of?(Errno::EEXIST) || # Windows/cygwin + $!.kind_of?(Errno::EACCES), # Windows + "Expected Errno::EISDIR (or on win/cygwin: Errno::EEXIST), but was: #{$!.class}") + end + end + + def assert_i_o_error_in_closed_stream + assert_raise(IOError) { + zos = ZipOutputStream.new("test_putOnClosedStream.zip") + zos.close + yield zos + } + end + + def write_test_zip(zos) + TEST_ZIP.entry_names.each { + |entryName| + zos.put_next_entry(entryName) + File.open(entryName, "rb") { |f| zos.write(f.read) } + } + end +end + + + +module Enumerable + def compare_enumerables(otherEnumerable) + otherAsArray = otherEnumerable.to_a + index=0 + each_with_index { + |element, index| + return false unless yield(element, otherAsArray[index]) + } + return index+1 == otherAsArray.size + end +end + + +class ZipCentralDirectoryEntryTest < Test::Unit::TestCase + + def test_read_from_stream + File.open("data/testDirectory.bin", "rb") { + |file| + entry = ZipEntry.read_c_dir_entry(file) + + assert_equal("longAscii.txt", entry.name) + assert_equal(ZipEntry::DEFLATED, entry.compression_method) + assert_equal(106490, entry.size) + assert_equal(3784, entry.compressed_size) + assert_equal(0xfcd1799c, entry.crc) + assert_equal("", entry.comment) + + entry = ZipEntry.read_c_dir_entry(file) + assert_equal("empty.txt", entry.name) + assert_equal(ZipEntry::STORED, entry.compression_method) + assert_equal(0, entry.size) + assert_equal(0, entry.compressed_size) + assert_equal(0x0, entry.crc) + assert_equal("", entry.comment) + + entry = ZipEntry.read_c_dir_entry(file) + assert_equal("short.txt", entry.name) + assert_equal(ZipEntry::STORED, entry.compression_method) + assert_equal(6, entry.size) + assert_equal(6, entry.compressed_size) + assert_equal(0xbb76fe69, entry.crc) + assert_equal("", entry.comment) + + entry = ZipEntry.read_c_dir_entry(file) + assert_equal("longBinary.bin", entry.name) + assert_equal(ZipEntry::DEFLATED, entry.compression_method) + assert_equal(1000024, entry.size) + assert_equal(70847, entry.compressed_size) + assert_equal(0x10da7d59, entry.crc) + assert_equal("", entry.comment) + + entry = ZipEntry.read_c_dir_entry(file) + assert_equal(nil, entry) +# Fields that are not check by this test: +# version made by 2 bytes +# version needed to extract 2 bytes +# general purpose bit flag 2 bytes +# last mod file time 2 bytes +# last mod file date 2 bytes +# compressed size 4 bytes +# uncompressed size 4 bytes +# disk number start 2 bytes +# internal file attributes 2 bytes +# external file attributes 4 bytes +# relative offset of local header 4 bytes + +# file name (variable size) +# extra field (variable size) +# file comment (variable size) + + } + end + + def test_ReadEntryFromTruncatedZipFile + fragment="" + File.open("data/testDirectory.bin") { |f| fragment = f.read(12) } # cdir entry header is at least 46 bytes + fragment.extend(IOizeString) + entry = ZipEntry.new + entry.read_c_dir_entry(fragment) + fail "ZipError expected" + rescue ZipError + end + +end + + +class ZipEntrySetTest < Test::Unit::TestCase + ZIP_ENTRIES = [ + ZipEntry.new("zipfile.zip", "name1", "comment1"), + ZipEntry.new("zipfile.zip", "name2", "comment1"), + ZipEntry.new("zipfile.zip", "name3", "comment1"), + ZipEntry.new("zipfile.zip", "name4", "comment1"), + ZipEntry.new("zipfile.zip", "name5", "comment1"), + ZipEntry.new("zipfile.zip", "name6", "comment1") + ] + + def setup + @zipEntrySet = ZipEntrySet.new(ZIP_ENTRIES) + end + + def test_include + assert(@zipEntrySet.include?(ZIP_ENTRIES.first)) + assert(! @zipEntrySet.include?(ZipEntry.new("different.zip", "different", "aComment"))) + end + + def test_size + assert_equal(ZIP_ENTRIES.size, @zipEntrySet.size) + assert_equal(ZIP_ENTRIES.size, @zipEntrySet.length) + @zipEntrySet << ZipEntry.new("a", "b", "c") + assert_equal(ZIP_ENTRIES.size + 1, @zipEntrySet.length) + end + + def test_add + zes = ZipEntrySet.new + entry1 = ZipEntry.new("zf.zip", "name1") + entry2 = ZipEntry.new("zf.zip", "name2") + zes << entry1 + assert(zes.include?(entry1)) + zes.push(entry2) + assert(zes.include?(entry2)) + end + + def test_delete + assert_equal(ZIP_ENTRIES.size, @zipEntrySet.size) + entry = @zipEntrySet.delete(ZIP_ENTRIES.first) + assert_equal(ZIP_ENTRIES.size - 1, @zipEntrySet.size) + assert_equal(ZIP_ENTRIES.first, entry) + + entry = @zipEntrySet.delete(ZIP_ENTRIES.first) + assert_equal(ZIP_ENTRIES.size - 1, @zipEntrySet.size) + assert_nil(entry) + end + + def test_each + # Tested indirectly via each_with_index + count = 0 + @zipEntrySet.each_with_index { + |entry, index| + assert(ZIP_ENTRIES.include?(entry)) + count = count.succ + } + assert_equal(ZIP_ENTRIES.size, count) + end + + def test_entries + assert_equal(ZIP_ENTRIES.sort, @zipEntrySet.entries.sort) + end + + def test_compound + newEntry = ZipEntry.new("zf.zip", "new entry", "new entry's comment") + assert_equal(ZIP_ENTRIES.size, @zipEntrySet.size) + @zipEntrySet << newEntry + assert_equal(ZIP_ENTRIES.size + 1, @zipEntrySet.size) + assert(@zipEntrySet.include?(newEntry)) + + @zipEntrySet.delete(newEntry) + assert_equal(ZIP_ENTRIES.size, @zipEntrySet.size) + end + + def test_dup + copy = @zipEntrySet.dup + assert_equal(@zipEntrySet, copy) + + # demonstrate that this is a deep copy + copy.entries[0].name = "a totally different name" + assert(@zipEntrySet != copy) + end + + def test_parent + entries = [ + ZipEntry.new("zf.zip", "a"), + ZipEntry.new("zf.zip", "a/"), + ZipEntry.new("zf.zip", "a/b"), + ZipEntry.new("zf.zip", "a/b/"), + ZipEntry.new("zf.zip", "a/b/c"), + ZipEntry.new("zf.zip", "a/b/c/") + ] + entrySet = ZipEntrySet.new(entries) + + assert_equal(nil, entrySet.parent(entries[0])) + assert_equal(nil, entrySet.parent(entries[1])) + assert_equal(entries[1], entrySet.parent(entries[2])) + assert_equal(entries[1], entrySet.parent(entries[3])) + assert_equal(entries[3], entrySet.parent(entries[4])) + assert_equal(entries[3], entrySet.parent(entries[5])) + end + + def test_glob + res = @zipEntrySet.glob('name[2-4]') + assert_equal(3, res.size) + assert_equal(ZIP_ENTRIES[1,3], res) + end + + def test_glob2 + entries = [ + ZipEntry.new("zf.zip", "a/"), + ZipEntry.new("zf.zip", "a/b/b1"), + ZipEntry.new("zf.zip", "a/b/c/"), + ZipEntry.new("zf.zip", "a/b/c/c1") + ] + entrySet = ZipEntrySet.new(entries) + + assert_equal(entries[0,1], entrySet.glob("*")) +# assert_equal(entries[FIXME], entrySet.glob("**")) +# res = entrySet.glob('a*') +# assert_equal(entries.size, res.size) +# assert_equal(entrySet.map { |e| e.name }, res.map { |e| e.name }) + end +end + + +class ZipCentralDirectoryTest < Test::Unit::TestCase + + def test_read_from_stream + File.open(TestZipFile::TEST_ZIP2.zip_name, "rb") { + |zipFile| + cdir = ZipCentralDirectory.read_from_stream(zipFile) + + assert_equal(TestZipFile::TEST_ZIP2.entry_names.size, cdir.size) + assert(cdir.entries.sort.compare_enumerables(TestZipFile::TEST_ZIP2.entry_names.sort) { + |cdirEntry, testEntryName| + cdirEntry.name == testEntryName + }) + assert_equal(TestZipFile::TEST_ZIP2.comment, cdir.comment) + } + end + + def test_readFromInvalidStream + File.open("data/file2.txt", "rb") { + |zipFile| + cdir = ZipCentralDirectory.new + cdir.read_from_stream(zipFile) + } + fail "ZipError expected!" + rescue ZipError + end + + def test_ReadFromTruncatedZipFile + fragment="" + File.open("data/testDirectory.bin") { |f| fragment = f.read } + fragment.slice!(12) # removed part of first cdir entry. eocd structure still complete + fragment.extend(IOizeString) + entry = ZipCentralDirectory.new + entry.read_from_stream(fragment) + fail "ZipError expected" + rescue ZipError + end + + def test_write_to_stream + entries = [ ZipEntry.new("file.zip", "flimse", "myComment", "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt", "Has a comment too") ] + cdir = ZipCentralDirectory.new(entries, "my zip comment") + File.open("cdirtest.bin", "wb") { |f| cdir.write_to_stream(f) } + cdirReadback = ZipCentralDirectory.new + File.open("cdirtest.bin", "rb") { |f| cdirReadback.read_from_stream(f) } + + assert_equal(cdir.entries.sort, cdirReadback.entries.sort) + end + + def test_equality + cdir1 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "my zip comment") + cdir2 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "my zip comment") + cdir3 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "comment?") + cdir4 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "comment?") + assert_equal(cdir1, cdir1) + assert_equal(cdir1, cdir2) + + assert(cdir1 != cdir3) + assert(cdir2 != cdir3) + assert(cdir2 != cdir3) + assert(cdir3 != cdir4) + + assert(cdir3 != "hello") + end +end + + +class BasicZipFileTest < Test::Unit::TestCase + include AssertEntry + + def setup + @zipFile = ZipFile.new(TestZipFile::TEST_ZIP2.zip_name) + @testEntryNameIndex=0 + end + + def test_entries + assert_equal(TestZipFile::TEST_ZIP2.entry_names.sort, + @zipFile.entries.entries.sort.map {|e| e.name} ) + end + + def test_each + count = 0 + visited = {} + @zipFile.each { + |entry| + assert(TestZipFile::TEST_ZIP2.entry_names.include?(entry.name)) + assert(! visited.include?(entry.name)) + visited[entry.name] = nil + count = count.succ + } + assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count) + end + + def test_foreach + count = 0 + visited = {} + ZipFile.foreach(TestZipFile::TEST_ZIP2.zip_name) { + |entry| + assert(TestZipFile::TEST_ZIP2.entry_names.include?(entry.name)) + assert(! visited.include?(entry.name)) + visited[entry.name] = nil + count = count.succ + } + assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count) + end + + def test_get_input_stream + count = 0 + visited = {} + @zipFile.each { + |entry| + assert_entry(entry.name, @zipFile.get_input_stream(entry), entry.name) + assert(! visited.include?(entry.name)) + visited[entry.name] = nil + count = count.succ + } + assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count) + end + + def test_get_input_streamBlock + fileAndEntryName = @zipFile.entries.first.name + @zipFile.get_input_stream(fileAndEntryName) { + |zis| + assert_entryContentsForStream(fileAndEntryName, + zis, + fileAndEntryName) + } + end +end + +module CommonZipFileFixture + include AssertEntry + + EMPTY_FILENAME = "emptyZipFile.zip" + + TEST_ZIP = TestZipFile::TEST_ZIP2.clone + TEST_ZIP.zip_name = "5entry_copy.zip" + + def setup + File.delete(EMPTY_FILENAME) if File.exists?(EMPTY_FILENAME) + File.copy(TestZipFile::TEST_ZIP2.zip_name, TEST_ZIP.zip_name) + end +end + +class ZipFileTest < Test::Unit::TestCase + include CommonZipFileFixture + + def test_createFromScratch + comment = "a short comment" + + zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) + zf.get_output_stream("myFile") { |os| os.write "myFile contains just this" } + zf.mkdir("dir1") + zf.comment = comment + zf.close + + zfRead = ZipFile.new(EMPTY_FILENAME) + assert_equal(comment, zfRead.comment) + assert_equal(2, zfRead.entries.length) + end + + def test_get_output_stream + entryCount = nil + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + entryCount = zf.size + zf.get_output_stream('newEntry.txt') { + |os| + os.write "Putting stuff in newEntry.txt" + } + assert_equal(entryCount+1, zf.size) + assert_equal("Putting stuff in newEntry.txt", zf.read("newEntry.txt")) + + zf.get_output_stream(zf.get_entry('data/generated/empty.txt')) { + |os| + os.write "Putting stuff in data/generated/empty.txt" + } + assert_equal(entryCount+1, zf.size) + assert_equal("Putting stuff in data/generated/empty.txt", zf.read("data/generated/empty.txt")) + + zf.get_output_stream('entry.bin') { + |os| + os.write(File.open('data/generated/5entry.zip', 'rb').read) + } + } + + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + assert_equal(entryCount+2, zf.size) + assert_equal("Putting stuff in newEntry.txt", zf.read("newEntry.txt")) + assert_equal("Putting stuff in data/generated/empty.txt", zf.read("data/generated/empty.txt")) + assert_equal(File.open('data/generated/5entry.zip', 'rb').read, zf.read("entry.bin")) + } + end + + def test_add + srcFile = "data/file2.txt" + entryName = "newEntryName.rb" + assert(File.exists?(srcFile)) + zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) + zf.add(entryName, srcFile) + zf.close + + zfRead = ZipFile.new(EMPTY_FILENAME) + assert_equal("", zfRead.comment) + assert_equal(1, zfRead.entries.length) + assert_equal(entryName, zfRead.entries.first.name) + AssertEntry.assert_contents(srcFile, + zfRead.get_input_stream(entryName) { |zis| zis.read }) + end + + def test_addExistingEntryName + assert_raise(ZipEntryExistsError) { + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.add(zf.entries.first.name, "data/file2.txt") + } + } + end + + def test_addExistingEntryNameReplace + gotCalled = false + replacedEntry = nil + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + replacedEntry = zf.entries.first.name + zf.add(replacedEntry, "data/file2.txt") { gotCalled = true; true } + } + assert(gotCalled) + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + assert_contains(zf, replacedEntry, "data/file2.txt") + } + end + + def test_addDirectory + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.add(TestFiles::EMPTY_TEST_DIR, TestFiles::EMPTY_TEST_DIR) + } + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + dirEntry = zf.entries.detect { |e| e.name == TestFiles::EMPTY_TEST_DIR+"/" } + assert(dirEntry.is_directory) + } + end + + def test_remove + entryToRemove, *remainingEntries = TEST_ZIP.entry_names + + File.copy(TestZipFile::TEST_ZIP2.zip_name, TEST_ZIP.zip_name) + + zf = ZipFile.new(TEST_ZIP.zip_name) + assert(zf.entries.map { |e| e.name }.include?(entryToRemove)) + zf.remove(entryToRemove) + assert(! zf.entries.map { |e| e.name }.include?(entryToRemove)) + assert_equal(zf.entries.map {|x| x.name }.sort, remainingEntries.sort) + zf.close + + zfRead = ZipFile.new(TEST_ZIP.zip_name) + assert(! zfRead.entries.map { |e| e.name }.include?(entryToRemove)) + assert_equal(zfRead.entries.map {|x| x.name }.sort, remainingEntries.sort) + zfRead.close + end + + + def test_rename + entryToRename, *remainingEntries = TEST_ZIP.entry_names + + zf = ZipFile.new(TEST_ZIP.zip_name) + assert(zf.entries.map { |e| e.name }.include?(entryToRename)) + + newName = "changed name" + assert(! zf.entries.map { |e| e.name }.include?(newName)) + + zf.rename(entryToRename, newName) + assert(zf.entries.map { |e| e.name }.include?(newName)) + + zf.close + + zfRead = ZipFile.new(TEST_ZIP.zip_name) + assert(zfRead.entries.map { |e| e.name }.include?(newName)) + zfRead.close + end + + def test_renameToExistingEntry + oldEntries = nil + ZipFile.open(TEST_ZIP.zip_name) { |zf| oldEntries = zf.entries } + + assert_raise(ZipEntryExistsError) { + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.rename(zf.entries[0], zf.entries[1].name) + } + } + + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + assert_equal(oldEntries.sort.map{ |e| e.name }, zf.entries.sort.map{ |e| e.name }) + } + end + + def test_renameToExistingEntryOverwrite + oldEntries = nil + ZipFile.open(TEST_ZIP.zip_name) { |zf| oldEntries = zf.entries } + + gotCalled = false + renamedEntryName = nil + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + renamedEntryName = zf.entries[0].name + zf.rename(zf.entries[0], zf.entries[1].name) { gotCalled = true; true } + } + + assert(gotCalled) + oldEntries.delete_if { |e| e.name == renamedEntryName } + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + assert_equal(oldEntries.sort.map{ |e| e.name }, + zf.entries.sort.map{ |e| e.name }) + } + end + + def test_renameNonEntry + nonEntry = "bogusEntry" + target_entry = "target_entryName" + zf = ZipFile.new(TEST_ZIP.zip_name) + assert(! zf.entries.include?(nonEntry)) + assert_raise(Errno::ENOENT) { + zf.rename(nonEntry, target_entry) + } + zf.commit + assert(! zf.entries.include?(target_entry)) + ensure + zf.close + end + + def test_renameEntryToExistingEntry + entry1, entry2, *remaining = TEST_ZIP.entry_names + zf = ZipFile.new(TEST_ZIP.zip_name) + assert_raise(ZipEntryExistsError) { + zf.rename(entry1, entry2) + } + ensure + zf.close + end + + def test_replace + entryToReplace = TEST_ZIP.entry_names[2] + newEntrySrcFilename = "data/file2.txt" + zf = ZipFile.new(TEST_ZIP.zip_name) + zf.replace(entryToReplace, newEntrySrcFilename) + + zf.close + zfRead = ZipFile.new(TEST_ZIP.zip_name) + AssertEntry::assert_contents(newEntrySrcFilename, + zfRead.get_input_stream(entryToReplace) { |is| is.read }) + AssertEntry::assert_contents(TEST_ZIP.entry_names[0], + zfRead.get_input_stream(TEST_ZIP.entry_names[0]) { |is| is.read }) + AssertEntry::assert_contents(TEST_ZIP.entry_names[1], + zfRead.get_input_stream(TEST_ZIP.entry_names[1]) { |is| is.read }) + AssertEntry::assert_contents(TEST_ZIP.entry_names[3], + zfRead.get_input_stream(TEST_ZIP.entry_names[3]) { |is| is.read }) + zfRead.close + end + + def test_replaceNonEntry + entryToReplace = "nonExistingEntryname" + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + assert_raise(Errno::ENOENT) { + zf.replace(entryToReplace, "data/file2.txt") + } + } + end + + def test_commit + newName = "renamedFirst" + zf = ZipFile.new(TEST_ZIP.zip_name) + oldName = zf.entries.first + zf.rename(oldName, newName) + zf.commit + + zfRead = ZipFile.new(TEST_ZIP.zip_name) + assert(zfRead.entries.detect { |e| e.name == newName } != nil) + assert(zfRead.entries.detect { |e| e.name == oldName } == nil) + zfRead.close + + zf.close + end + + # This test tests that after commit, you + # can delete the file you used to add the entry to the zip file + # with + def test_commitUseZipEntry + File.copy(TestFiles::RANDOM_ASCII_FILE1, "okToDelete.txt") + zf = ZipFile.open(TEST_ZIP.zip_name) + zf.add("okToDelete.txt", "okToDelete.txt") + assert_contains(zf, "okToDelete.txt") + zf.commit + File.move("okToDelete.txt", "okToDeleteMoved.txt") + assert_contains(zf, "okToDelete.txt", "okToDeleteMoved.txt") + end + +# def test_close +# zf = ZipFile.new(TEST_ZIP.zip_name) +# zf.close +# assert_raise(IOError) { +# zf.extract(TEST_ZIP.entry_names.first, "hullubullu") +# } +# end + + def test_compound1 + renamedName = "renamedName" + originalEntries = [] + begin + zf = ZipFile.new(TEST_ZIP.zip_name) + originalEntries = zf.entries.dup + + assert_not_contains(zf, TestFiles::RANDOM_ASCII_FILE1) + zf.add(TestFiles::RANDOM_ASCII_FILE1, + TestFiles::RANDOM_ASCII_FILE1) + assert_contains(zf, TestFiles::RANDOM_ASCII_FILE1) + + zf.rename(zf.entries[0], renamedName) + assert_contains(zf, renamedName) + + TestFiles::BINARY_TEST_FILES.each { + |filename| + zf.add(filename, filename) + assert_contains(zf, filename) + } + + assert_contains(zf, originalEntries.last.to_s) + zf.remove(originalEntries.last.to_s) + assert_not_contains(zf, originalEntries.last.to_s) + + ensure + zf.close + end + begin + zfRead = ZipFile.new(TEST_ZIP.zip_name) + assert_contains(zfRead, TestFiles::RANDOM_ASCII_FILE1) + assert_contains(zfRead, renamedName) + TestFiles::BINARY_TEST_FILES.each { + |filename| + assert_contains(zfRead, filename) + } + assert_not_contains(zfRead, originalEntries.last.to_s) + ensure + zfRead.close + end + end + + def test_compound2 + begin + zf = ZipFile.new(TEST_ZIP.zip_name) + originalEntries = zf.entries.dup + + originalEntries.each { + |entry| + zf.remove(entry) + assert_not_contains(zf, entry) + } + assert(zf.entries.empty?) + + TestFiles::ASCII_TEST_FILES.each { + |filename| + zf.add(filename, filename) + assert_contains(zf, filename) + } + assert_equal(zf.entries.sort.map { |e| e.name }, TestFiles::ASCII_TEST_FILES) + + zf.rename(TestFiles::ASCII_TEST_FILES[0], "newName") + assert_not_contains(zf, TestFiles::ASCII_TEST_FILES[0]) + assert_contains(zf, "newName") + ensure + zf.close + end + begin + zfRead = ZipFile.new(TEST_ZIP.zip_name) + asciiTestFiles = TestFiles::ASCII_TEST_FILES.dup + asciiTestFiles.shift + asciiTestFiles.each { + |filename| + assert_contains(zf, filename) + } + + assert_contains(zf, "newName") + ensure + zfRead.close + end + end + + private + def assert_contains(zf, entryName, filename = entryName) + assert(zf.entries.detect { |e| e.name == entryName} != nil, "entry #{entryName} not in #{zf.entries.join(', ')} in zip file #{zf}") + assert_entryContents(zf, entryName, filename) if File.exists?(filename) + end + + def assert_not_contains(zf, entryName) + assert(zf.entries.detect { |e| e.name == entryName} == nil, "entry #{entryName} in #{zf.entries.join(', ')} in zip file #{zf}") + end +end + +class ZipFileExtractTest < Test::Unit::TestCase + include CommonZipFileFixture + EXTRACTED_FILENAME = "extEntry" + ENTRY_TO_EXTRACT, *REMAINING_ENTRIES = TEST_ZIP.entry_names.reverse + + def setup + super + File.delete(EXTRACTED_FILENAME) if File.exists?(EXTRACTED_FILENAME) + end + + def test_extract + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.extract(ENTRY_TO_EXTRACT, EXTRACTED_FILENAME) + + assert(File.exists?(EXTRACTED_FILENAME)) + AssertEntry::assert_contents(EXTRACTED_FILENAME, + zf.get_input_stream(ENTRY_TO_EXTRACT) { |is| is.read }) + + + File::unlink(EXTRACTED_FILENAME) + + entry = zf.get_entry(ENTRY_TO_EXTRACT) + entry.extract(EXTRACTED_FILENAME) + + assert(File.exists?(EXTRACTED_FILENAME)) + AssertEntry::assert_contents(EXTRACTED_FILENAME, + entry.get_input_stream() { |is| is.read }) + + } + end + + def test_extractExists + writtenText = "written text" + File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } + + assert_raise(ZipDestinationFileExistsError) { + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.extract(zf.entries.first, EXTRACTED_FILENAME) + } + } + File.open(EXTRACTED_FILENAME, "r") { + |f| + assert_equal(writtenText, f.read) + } + end + + def test_extractExistsOverwrite + writtenText = "written text" + File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } + + gotCalledCorrectly = false + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.extract(zf.entries.first, EXTRACTED_FILENAME) { + |entry, extractLoc| + gotCalledCorrectly = zf.entries.first == entry && + extractLoc == EXTRACTED_FILENAME + true + } + } + + assert(gotCalledCorrectly) + File.open(EXTRACTED_FILENAME, "r") { + |f| + assert(writtenText != f.read) + } + end + + def test_extractNonEntry + zf = ZipFile.new(TEST_ZIP.zip_name) + assert_raise(Errno::ENOENT) { zf.extract("nonExistingEntry", "nonExistingEntry") } + ensure + zf.close if zf + end + + def test_extractNonEntry2 + outFile = "outfile" + assert_raise(Errno::ENOENT) { + zf = ZipFile.new(TEST_ZIP.zip_name) + nonEntry = "hotdog-diddelidoo" + assert(! zf.entries.include?(nonEntry)) + zf.extract(nonEntry, outFile) + zf.close + } + assert(! File.exists?(outFile)) + end + +end + +class ZipFileExtractDirectoryTest < Test::Unit::TestCase + include CommonZipFileFixture + TEST_OUT_NAME = "emptyOutDir" + + def open_zip(&aProc) + assert(aProc != nil) + ZipFile.open(TestZipFile::TEST_ZIP4.zip_name, &aProc) + end + + def extract_test_dir(&aProc) + open_zip { + |zf| + zf.extract(TestFiles::EMPTY_TEST_DIR, TEST_OUT_NAME, &aProc) + } + end + + def setup + super + + Dir.rmdir(TEST_OUT_NAME) if File.directory? TEST_OUT_NAME + File.delete(TEST_OUT_NAME) if File.exists? TEST_OUT_NAME + end + + def test_extractDirectory + extract_test_dir + assert(File.directory?(TEST_OUT_NAME)) + end + + def test_extractDirectoryExistsAsDir + Dir.mkdir TEST_OUT_NAME + extract_test_dir + assert(File.directory?(TEST_OUT_NAME)) + end + + def test_extractDirectoryExistsAsFile + File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } + assert_raise(ZipDestinationFileExistsError) { extract_test_dir } + end + + def test_extractDirectoryExistsAsFileOverwrite + File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } + gotCalled = false + extract_test_dir { + |entry, destPath| + gotCalled = true + assert_equal(TEST_OUT_NAME, destPath) + assert(entry.is_directory) + true + } + assert(gotCalled) + assert(File.directory?(TEST_OUT_NAME)) + end +end + +class ZipExtraFieldTest < Test::Unit::TestCase + def test_new + extra_pure = ZipExtraField.new("") + extra_withstr = ZipExtraField.new("foo") + assert_instance_of(ZipExtraField, extra_pure) + assert_instance_of(ZipExtraField, extra_withstr) + end + + def test_unknownfield + extra = ZipExtraField.new("foo") + assert_equal(extra["Unknown"], "foo") + extra.merge("a") + assert_equal(extra["Unknown"], "fooa") + extra.merge("barbaz") + assert_equal(extra.to_s, "fooabarbaz") + end + + + def test_merge + str = "UT\x5\0\x3\250$\r@Ux\0\0" + extra1 = ZipExtraField.new("") + extra2 = ZipExtraField.new(str) + assert(! extra1.member?("UniversalTime")) + assert(extra2.member?("UniversalTime")) + extra1.merge(str) + assert_equal(extra1["UniversalTime"].mtime, extra2["UniversalTime"].mtime) + end + + def test_length + str = "UT\x5\0\x3\250$\r@Ux\0\0Te\0\0testit" + extra = ZipExtraField.new(str) + assert_equal(extra.local_length, extra.to_local_bin.length) + assert_equal(extra.c_dir_length, extra.to_c_dir_bin.length) + extra.merge("foo") + assert_equal(extra.local_length, extra.to_local_bin.length) + assert_equal(extra.c_dir_length, extra.to_c_dir_bin.length) + end + + + def test_to_s + str = "UT\x5\0\x3\250$\r@Ux\0\0Te\0\0testit" + extra = ZipExtraField.new(str) + assert_instance_of(String, extra.to_s) + + s = extra.to_s + extra.merge("foo") + assert_equal(s.length + 3, extra.to_s.length) + end + + def test_equality + str = "UT\x5\0\x3\250$\r@" + extra1 = ZipExtraField.new(str) + extra2 = ZipExtraField.new(str) + extra3 = ZipExtraField.new(str) + assert_equal(extra1, extra2) + + extra2["UniversalTime"].mtime = Time.now + assert(extra1 != extra2) + + extra3.create("IUnix") + assert(extra1 != extra3) + + extra1.create("IUnix") + assert_equal(extra1, extra3) + end + +end + +# Copyright (C) 2002-2005 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/sqlite3-ruby/sqlite3.rb b/vendor/plugins/sqlite3-ruby/sqlite3.rb new file mode 100644 index 00000000..ff8af026 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3.rb @@ -0,0 +1,33 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/database' diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/constants.rb b/vendor/plugins/sqlite3-ruby/sqlite3/constants.rb new file mode 100644 index 00000000..5a20e461 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/constants.rb @@ -0,0 +1,81 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +module SQLite3 ; module Constants + + module TextRep + UTF8 = 1 + UTF16LE = 2 + UTF16BE = 3 + UTF16 = 4 + ANY = 5 + end + + module ColumnType + INTEGER = 1 + FLOAT = 2 + TEXT = 3 + BLOB = 4 + NULL = 5 + end + + module ErrorCode + OK = 0 # Successful result + ERROR = 1 # SQL error or missing database + INTERNAL = 2 # An internal logic error in SQLite + PERM = 3 # Access permission denied + ABORT = 4 # Callback routine requested an abort + BUSY = 5 # The database file is locked + LOCKED = 6 # A table in the database is locked + NOMEM = 7 # A malloc() failed + READONLY = 8 # Attempt to write a readonly database + INTERRUPT = 9 # Operation terminated by sqlite_interrupt() + IOERR = 10 # Some kind of disk I/O error occurred + CORRUPT = 11 # The database disk image is malformed + NOTFOUND = 12 # (Internal Only) Table or record not found + FULL = 13 # Insertion failed because database is full + CANTOPEN = 14 # Unable to open the database file + PROTOCOL = 15 # Database lock protocol error + EMPTY = 16 # (Internal Only) Database table is empty + SCHEMA = 17 # The database schema changed + TOOBIG = 18 # Too much data for one row of a table + CONSTRAINT = 19 # Abort due to contraint violation + MISMATCH = 20 # Data type mismatch + MISUSE = 21 # Library used incorrectly + NOLFS = 22 # Uses OS features not supported on host + AUTH = 23 # Authorization denied + + ROW = 100 # sqlite_step() has another row ready + DONE = 101 # sqlite_step() has finished executing + end + +end ; end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/database.rb b/vendor/plugins/sqlite3-ruby/sqlite3/database.rb new file mode 100644 index 00000000..6a130f90 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/database.rb @@ -0,0 +1,745 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'base64' +require 'sqlite3/constants' +require 'sqlite3/errors' +require 'sqlite3/pragmas' +require 'sqlite3/statement' +require 'sqlite3/translator' +require 'sqlite3/value' + +module SQLite3 + + # The Database class encapsulates a single connection to a SQLite3 database. + # Its usage is very straightforward: + # + # require 'sqlite3' + # + # db = SQLite3::Database.new( "data.db" ) + # + # db.execute( "select * from table" ) do |row| + # p row + # end + # + # db.close + # + # It wraps the lower-level methods provides by the selected driver, and + # includes the Pragmas module for access to various pragma convenience + # methods. + # + # The Database class provides type translation services as well, by which + # the SQLite3 data types (which are all represented as strings) may be + # converted into their corresponding types (as defined in the schemas + # for their tables). This translation only occurs when querying data from + # the database--insertions and updates are all still typeless. + # + # Furthermore, the Database class has been designed to work well with the + # ArrayFields module from Ara Howard. If you require the ArrayFields + # module before performing a query, and if you have not enabled results as + # hashes, then the results will all be indexible by field name. + class Database + include Pragmas + + class < e + @driver.result_error( func, + "#{e.message} (#{e.class})", -1 ) + end + end + + result = @driver.create_function( @handle, name, arity, text_rep, nil, + callback, nil, nil ) + Error.check( result, self ) + + self + end + + # Creates a new aggregate function for use in SQL statements. Aggregate + # functions are functions that apply over every row in the result set, + # instead of over just a single row. (A very common aggregate function + # is the "count" function, for determining the number of rows that match + # a query.) + # + # The new function will be added as +name+, with the given +arity+. (For + # variable arity functions, use -1 for the arity.) + # + # The +step+ parameter must be a proc object that accepts as its first + # parameter a FunctionProxy instance (representing the function + # invocation), with any subsequent parameters (up to the function's arity). + # The +step+ callback will be invoked once for each row of the result set. + # + # The +finalize+ parameter must be a +proc+ object that accepts only a + # single parameter, the FunctionProxy instance representing the current + # function invocation. It should invoke FunctionProxy#set_result to + # store the result of the function. + # + # Example: + # + # db.create_aggregate( "lengths", 1 ) do + # step do |func, value| + # func[ :total ] ||= 0 + # func[ :total ] += ( value ? value.length : 0 ) + # end + # + # finalize do |func| + # func.set_result( func[ :total ] || 0 ) + # end + # end + # + # puts db.get_first_value( "select lengths(name) from table" ) + # + # See also #create_aggregate_handler for a more object-oriented approach to + # aggregate functions. + def create_aggregate( name, arity, step=nil, finalize=nil, + text_rep=Constants::TextRep::ANY, &block ) + # begin + if block + proxy = AggregateDefinitionProxy.new + proxy.instance_eval(&block) + step ||= proxy.step_callback + finalize ||= proxy.finalize_callback + end + + step_callback = proc do |func,*args| + ctx = @driver.aggregate_context( func ) + unless ctx[:__error] + begin + step.call( FunctionProxy.new( @driver, func, ctx ), + *args.map{|v| Value.new(self,v)} ) + rescue Exception => e + ctx[:__error] = e + end + end + end + + finalize_callback = proc do |func| + ctx = @driver.aggregate_context( func ) + unless ctx[:__error] + begin + finalize.call( FunctionProxy.new( @driver, func, ctx ) ) + rescue Exception => e + @driver.result_error( func, + "#{e.message} (#{e.class})", -1 ) + end + else + e = ctx[:__error] + @driver.result_error( func, + "#{e.message} (#{e.class})", -1 ) + end + end + + result = @driver.create_function( @handle, name, arity, text_rep, nil, + nil, step_callback, finalize_callback ) + Error.check( result, self ) + + self + end + + # This is another approach to creating an aggregate function (see + # #create_aggregate). Instead of explicitly specifying the name, + # callbacks, arity, and type, you specify a factory object + # (the "handler") that knows how to obtain all of that information. The + # handler should respond to the following messages: + # + # +arity+:: corresponds to the +arity+ parameter of #create_aggregate. This + # message is optional, and if the handler does not respond to it, + # the function will have an arity of -1. + # +name+:: this is the name of the function. The handler _must_ implement + # this message. + # +new+:: this must be implemented by the handler. It should return a new + # instance of the object that will handle a specific invocation of + # the function. + # + # The handler instance (the object returned by the +new+ message, described + # above), must respond to the following messages: + # + # +step+:: this is the method that will be called for each step of the + # aggregate function's evaluation. It should implement the same + # signature as the +step+ callback for #create_aggregate. + # +finalize+:: this is the method that will be called to finalize the + # aggregate function's evaluation. It should implement the + # same signature as the +finalize+ callback for + # #create_aggregate. + # + # Example: + # + # class LengthsAggregateHandler + # def self.arity; 1; end + # + # def initialize + # @total = 0 + # end + # + # def step( ctx, name ) + # @total += ( name ? name.length : 0 ) + # end + # + # def finalize( ctx ) + # ctx.set_result( @total ) + # end + # end + # + # db.create_aggregate_handler( LengthsAggregateHandler ) + # puts db.get_first_value( "select lengths(name) from A" ) + def create_aggregate_handler( handler ) + arity = -1 + text_rep = Constants::TextRep::ANY + + arity = handler.arity if handler.respond_to?(:arity) + text_rep = handler.text_rep if handler.respond_to?(:text_rep) + name = handler.name + + step = proc do |func,*args| + ctx = @driver.aggregate_context( func ) + unless ctx[ :__error ] + ctx[ :handler ] ||= handler.new + begin + ctx[ :handler ].step( FunctionProxy.new( @driver, func, ctx ), + *args.map{|v| Value.new(self,v)} ) + rescue Exception, StandardError => e + ctx[ :__error ] = e + end + end + end + + finalize = proc do |func| + ctx = @driver.aggregate_context( func ) + unless ctx[ :__error ] + ctx[ :handler ] ||= handler.new + begin + ctx[ :handler ].finalize( FunctionProxy.new( @driver, func, ctx ) ) + rescue Exception => e + ctx[ :__error ] = e + end + end + + if ctx[ :__error ] + e = ctx[ :__error ] + @driver.sqlite3_result_error( func, "#{e.message} (#{e.class})", -1 ) + end + end + + result = @driver.create_function( @handle, name, arity, text_rep, nil, + nil, step, finalize ) + Error.check( result, self ) + + self + end + + # Begins a new transaction. Note that nested transactions are not allowed + # by SQLite, so attempting to nest a transaction will result in a runtime + # exception. + # + # The +mode+ parameter may be either :deferred (the default), + # :immediate, or :exclusive. + # + # If a block is given, the database instance is yielded to it, and the + # transaction is committed when the block terminates. If the block + # raises an exception, a rollback will be performed instead. Note that if + # a block is given, #commit and #rollback should never be called + # explicitly or you'll get an error when the block terminates. + # + # If a block is not given, it is the caller's responsibility to end the + # transaction explicitly, either by calling #commit, or by calling + # #rollback. + def transaction( mode = :deferred ) + execute "begin #{mode.to_s} transaction" + @transaction_active = true + + if block_given? + abort = false + begin + yield self + rescue ::Object + abort = true + raise + ensure + abort and rollback or commit + end + end + + true + end + + # Commits the current transaction. If there is no current transaction, + # this will cause an error to be raised. This returns +true+, in order + # to allow it to be used in idioms like + # abort? and rollback or commit. + def commit + execute "commit transaction" + @transaction_active = false + true + end + + # Rolls the current transaction back. If there is no current transaction, + # this will cause an error to be raised. This returns +true+, in order + # to allow it to be used in idioms like + # abort? and rollback or commit. + def rollback + execute "rollback transaction" + @transaction_active = false + true + end + + # Returns +true+ if there is a transaction active, and +false+ otherwise. + def transaction_active? + @transaction_active + end + + # Loads the corresponding driver, or if it is nil, attempts to locate a + # suitable driver. + def load_driver( driver ) + case driver + when Class + # do nothing--use what was given + when Symbol, String + require "sqlite3/driver/#{driver.to_s.downcase}/driver" + driver = SQLite3::Driver.const_get( driver )::Driver + else + [ "Native", "DL" ].each do |d| + begin + require "sqlite3/driver/#{d.downcase}/driver" + driver = SQLite3::Driver.const_get( d )::Driver + break + rescue SyntaxError + raise + rescue ScriptError, Exception, NameError + end + end + raise "no driver for sqlite3 found" unless driver + end + + @driver = driver.new + end + private :load_driver + + # A helper class for dealing with custom functions (see #create_function, + # #create_aggregate, and #create_aggregate_handler). It encapsulates the + # opaque function object that represents the current invocation. It also + # provides more convenient access to the API functions that operate on + # the function object. + # + # This class will almost _always_ be instantiated indirectly, by working + # with the create methods mentioned above. + class FunctionProxy + + # Create a new FunctionProxy that encapsulates the given +func+ object. + # If context is non-nil, the functions context will be set to that. If + # it is non-nil, it must quack like a Hash. If it is nil, then none of + # the context functions will be available. + def initialize( driver, func, context=nil ) + @driver = driver + @func = func + @context = context + end + + # Calls #set_result to set the result of this function. + def result=( result ) + set_result( result ) + end + + # Set the result of the function to the given value. The function will + # then return this value. + def set_result( result, utf16=false ) + @driver.result_text( @func, result, utf16 ) + end + + # Set the result of the function to the given error message. + # The function will then return that error. + def set_error( error ) + @driver.result_error( @func, error.to_s, -1 ) + end + + # (Only available to aggregate functions.) Returns the number of rows + # that the aggregate has processed so far. This will include the current + # row, and so will always return at least 1. + def count + ensure_aggregate! + @driver.aggregate_count( @func ) + end + + # Returns the value with the given key from the context. This is only + # available to aggregate functions. + def []( key ) + ensure_aggregate! + @context[ key ] + end + + # Sets the value with the given key in the context. This is only + # available to aggregate functions. + def []=( key, value ) + ensure_aggregate! + @context[ key ] = value + end + + # A function for performing a sanity check, to ensure that the function + # being invoked is an aggregate function. This is implied by the + # existence of the context variable. + def ensure_aggregate! + unless @context + raise MisuseException, "function is not an aggregate" + end + end + private :ensure_aggregate! + + end + + # A proxy used for defining the callbacks to an aggregate function. + class AggregateDefinitionProxy # :nodoc: + attr_reader :step_callback, :finalize_callback + + def step( &block ) + @step_callback = block + end + + def finalize( &block ) + @finalize_callback = block + end + end + + end + +end + diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/driver/dl/api.rb b/vendor/plugins/sqlite3-ruby/sqlite3/driver/dl/api.rb new file mode 100644 index 00000000..9cea866f --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/driver/dl/api.rb @@ -0,0 +1,184 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'dl/import' + +module SQLite3 ; module Driver; module DL; + + module API + extend ::DL::Importable + + library_name = case RUBY_PLATFORM.downcase + when /darwin/ + "libsqlite3.dylib" + when /linux/, /freebsd|netbsd|openbsd|dragonfly/, /solaris/ + "libsqlite3.so" + when /win32/ + "sqlite3.dll" + else + abort <<-EOF +== * UNSUPPORTED PLATFORM ====================================================== +The platform '#{RUBY_PLATFORM}' is unsupported. Please help the author by +editing the following file to allow your sqlite3 library to be found, and +submitting a patch to jamis_buck@byu.edu. Thanks! + +#{__FILE__} +=========================================================================== * == + EOF + end + + if defined? SQLITE3_LIB_PATH + library_name = File.join( SQLITE3_LIB_PATH, library_name ) + end + + dlload library_name + + typealias "db", "void*" + typealias "stmt", "void*" + typealias "value", "void*" + typealias "context", "void*" + + # until Ruby/DL supports 64-bit ints, we'll just treat them as 32-bit ints + typealias "int64", "unsigned long" + + extern "const char *sqlite3_libversion()" + + extern "int sqlite3_open(const char*,db*)" + extern "int sqlite3_open16(const void*,db*)" + extern "int sqlite3_close(db)" + extern "const char* sqlite3_errmsg(db)" + extern "void* sqlite3_errmsg16(db)" + extern "int sqlite3_errcode(db)" + + extern "int sqlite3_prepare(db,const char*,int,stmt*,const char**)" + extern "int sqlite3_prepare16(db,const void*,int,stmt*,const void**)" + extern "int sqlite3_finalize(stmt)" + extern "int sqlite3_reset(stmt)" + extern "int sqlite3_step(stmt)" + + extern "int64 sqlite3_last_insert_rowid(db)" + extern "int sqlite3_changes(db)" + extern "int sqlite3_total_changes(db)" + extern "void sqlite3_interrupt(db)" + extern "ibool sqlite3_complete(const char*)" + extern "ibool sqlite3_complete16(const void*)" + + extern "int sqlite3_busy_handler(db,void*,void*)" + extern "int sqlite3_busy_timeout(db,int)" + + extern "int sqlite3_set_authorizer(db,void*,void*)" + extern "void* sqlite3_trace(db,void*,void*)" + + extern "int sqlite3_bind_blob(stmt,int,const void*,int,void*)" + extern "int sqlite3_bind_double(stmt,int,double)" + extern "int sqlite3_bind_int(stmt,int,int)" + extern "int sqlite3_bind_int64(stmt,int,int64)" + extern "int sqlite3_bind_null(stmt,int)" + extern "int sqlite3_bind_text(stmt,int,const char*,int,void*)" + extern "int sqlite3_bind_text16(stmt,int,const void*,int,void*)" + #extern "int sqlite3_bind_value(stmt,int,value)" + + extern "int sqlite3_bind_parameter_count(stmt)" + extern "const char* sqlite3_bind_parameter_name(stmt,int)" + extern "int sqlite3_bind_parameter_index(stmt,const char*)" + + extern "int sqlite3_column_count(stmt)" + extern "int sqlite3_data_count(stmt)" + + extern "const void *sqlite3_column_blob(stmt,int)" + extern "int sqlite3_column_bytes(stmt,int)" + extern "int sqlite3_column_bytes16(stmt,int)" + extern "const char *sqlite3_column_decltype(stmt,int)" + extern "void *sqlite3_column_decltype16(stmt,int)" + extern "double sqlite3_column_double(stmt,int)" + extern "int sqlite3_column_int(stmt,int)" + extern "int64 sqlite3_column_int64(stmt,int)" + extern "const char *sqlite3_column_name(stmt,int)" + extern "const void *sqlite3_column_name16(stmt,int)" + extern "const char *sqlite3_column_text(stmt,int)" + extern "const void *sqlite3_column_text16(stmt,int)" + extern "int sqlite3_column_type(stmt,int)" + + extern "int sqlite3_create_function(db,const char*,int,int,void*,void*,void*,void*)" + extern "int sqlite3_create_function16(db,const void*,int,int,void*,void*,void*,void*)" + extern "int sqlite3_aggregate_count(context)" + + extern "const void *sqlite3_value_blob(value)" + extern "int sqlite3_value_bytes(value)" + extern "int sqlite3_value_bytes16(value)" + extern "double sqlite3_value_double(value)" + extern "int sqlite3_value_int(value)" + extern "int64 sqlite3_value_int64(value)" + extern "const char* sqlite3_value_text(value)" + extern "const void* sqlite3_value_text16(value)" + extern "const void* sqlite3_value_text16le(value)" + extern "const void* sqlite3_value_text16be(value)" + extern "int sqlite3_value_type(value)" + + extern "void *sqlite3_aggregate_context(context,int)" + extern "void *sqlite3_user_data(context)" + extern "void *sqlite3_get_auxdata(context,int)" + extern "void sqlite3_set_auxdata(context,int,void*,void*)" + + extern "void sqlite3_result_blob(context,const void*,int,void*)" + extern "void sqlite3_result_double(context,double)" + extern "void sqlite3_result_error(context,const char*,int)" + extern "void sqlite3_result_error16(context,const void*,int)" + extern "void sqlite3_result_int(context,int)" + extern "void sqlite3_result_int64(context,int64)" + extern "void sqlite3_result_null(context)" + extern "void sqlite3_result_text(context,const char*,int,void*)" + extern "void sqlite3_result_text16(context,const void*,int,void*)" + extern "void sqlite3_result_text16le(context,const void*,int,void*)" + extern "void sqlite3_result_text16be(context,const void*,int,void*)" + extern "void sqlite3_result_value(context,value)" + + extern "int sqlite3_create_collation(db,const char*,int,void*,void*)" + extern "int sqlite3_create_collation16(db,const char*,int,void*,void*)" + extern "int sqlite3_collation_needed(db,void*,void*)" + extern "int sqlite3_collation_needed16(db,void*,void*)" + + # ==== CRYPTO (NOT IN PUBLIC RELEASE) ==== + if defined?( CRYPTO_API ) && CRYPTO_API + extern "int sqlite3_key(db,void*,int)" + extern "int sqlite3_rekey(db,void*,int)" + end + + # ==== EXPERIMENTAL ==== + if defined?( EXPERIMENTAL_API ) && EXPERIMENTAL_API + extern "int sqlite3_progress_handler(db,int,void*,void*)" + extern "int sqlite3_commit_hook(db,void*,void*)" + end + + end + +end ; end ; end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/driver/dl/driver.rb b/vendor/plugins/sqlite3-ruby/sqlite3/driver/dl/driver.rb new file mode 100644 index 00000000..ac72e0db --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/driver/dl/driver.rb @@ -0,0 +1,338 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/driver/dl/api' + +warn "The DL driver for sqlite3-ruby is deprecated and will be removed" +warn "in a future release. Please update your installation to use the" +warn "Native driver." + +module Kernel + # Allows arbitrary objects to be passed as a pointer to functions. + # (Probably not very GC safe, but by encapsulating it like this we + # can change the implementation later.) + def to_ptr + ptr = DL.malloc(DL.sizeof("L")) + ptr.set_object self + ptr + end +end + +class DL::PtrData + # The inverse of the Kernel#to_ptr operation. + def to_object + n = to_s(4).unpack("L").first + return nil if n < 1 + ObjectSpace._id2ref(n) rescue self.to_s + end + + def set_object(obj) + self[0] = [obj.object_id].pack("L") + end +end + +module SQLite3 ; module Driver ; module DL + + class Driver + STATIC = ::DL::PtrData.new(0) + TRANSIENT = ::DL::PtrData.new(-1) + + def open( filename, utf16=false ) + handle = ::DL::PtrData.new(0) + result = API.send( ( utf16 ? :sqlite3_open16 : :sqlite3_open ), + filename+"\0", handle.ref ) + [ result, handle ] + end + + def errmsg( db, utf16=false ) + if utf16 + msg = API.sqlite3_errmsg16( db ) + msg.free = nil + msg.to_s(utf16_length(msg)) + else + API.sqlite3_errmsg( db ) + end + end + + def prepare( db, sql, utf16=false ) + handle = ::DL::PtrData.new(0) + remainder = ::DL::PtrData.new(0) + + result = API.send( ( utf16 ? :sqlite3_prepare16 : :sqlite3_prepare ), + db, sql+"\0", sql.length, handle.ref, remainder.ref ) + + args = utf16 ? [ utf16_length(remainder) ] : [] + remainder = remainder.to_s( *args ) + + [ result, handle, remainder ] + end + + def complete?( sql, utf16=false ) + API.send( utf16 ? :sqlite3_complete16 : :sqlite3_complete, sql+"\0" ) + end + + def value_blob( value ) + blob = API.sqlite3_value_blob( value ) + blob.free = nil + blob.to_s( API.sqlite3_value_bytes( value ) ) + end + + def value_text( value, utf16=false ) + method = case utf16 + when nil, false then :sqlite3_value_text + when :le then :sqlite3_value_text16le + when :be then :sqlite3_value_text16be + else :sqlite3_value_text16 + end + + result = API.send( method, value ) + if utf16 + result.free = nil + size = API.sqlite3_value_bytes( value ) + result = result.to_s( size ) + end + + result + end + + def column_blob( stmt, column ) + blob = API.sqlite3_column_blob( stmt, column ) + blob.free = nil + blob.to_s( API.sqlite3_column_bytes( stmt, column ) ) + end + + def result_text( func, text, utf16=false ) + method = case utf16 + when false, nil then :sqlite3_result_text + when :le then :sqlite3_result_text16le + when :be then :sqlite3_result_text16be + else :sqlite3_result_text16 + end + + s = text.to_s + API.send( method, func, s, s.length, TRANSIENT ) + end + + def busy_handler( db, data=nil, &block ) + @busy_handler = block + + unless @busy_handler_callback + @busy_handler_callback = ::DL.callback( "IPI" ) do |cookie, timeout| + @busy_handler.call( cookie, timeout ) || 0 + end + end + + API.sqlite3_busy_handler( db, block&&@busy_handler_callback, data ) + end + + def set_authorizer( db, data=nil, &block ) + @authorizer_handler = block + + unless @authorizer_handler_callback + @authorizer_handler_callback = ::DL.callback( "IPIPPPP" + ) do |cookie,mode,a,b,c,d| + @authorizer_handler.call( cookie, mode, + a&&a.to_s, b&&b.to_s, c&&c.to_s, d&&d.to_s ) || 0 + end + end + + API.sqlite3_set_authorizer( db, block&&@authorizer_handler_callback, + data ) + end + + def trace( db, data=nil, &block ) + @trace_handler = block + + unless @trace_handler_callback + @trace_handler_callback = ::DL.callback( "IPS" ) do |cookie,sql| + @trace_handler.call( cookie ? cookie.to_object : nil, sql ) || 0 + end + end + + API.sqlite3_trace( db, block&&@trace_handler_callback, data ) + end + + def create_function( db, name, args, text, cookie, + func, step, final ) + # begin + if @func_handler_callback.nil? && func + @func_handler_callback = ::DL.callback( "0PIP" ) do |context,nargs,args| + args = args.to_s(nargs*4).unpack("L*").map {|i| ::DL::PtrData.new(i)} + data = API.sqlite3_user_data( context ).to_object + data[:func].call( context, *args ) + end + end + + if @step_handler_callback.nil? && step + @step_handler_callback = ::DL.callback( "0PIP" ) do |context,nargs,args| + args = args.to_s(nargs*4).unpack("L*").map {|i| ::DL::PtrData.new(i)} + data = API.sqlite3_user_data( context ).to_object + data[:step].call( context, *args ) + end + end + + if @final_handler_callback.nil? && final + @final_handler_callback = ::DL.callback( "0P" ) do |context| + data = API.sqlite3_user_data( context ).to_object + data[:final].call( context ) + end + end + + data = { :cookie => cookie, + :name => name, + :func => func, + :step => step, + :final => final } + + API.sqlite3_create_function( db, name, args, text, data, + ( func ? @func_handler_callback : nil ), + ( step ? @step_handler_callback : nil ), + ( final ? @final_handler_callback : nil ) ) + end + + def aggregate_context( context ) + ptr = API.sqlite3_aggregate_context( context, 4 ) + ptr.free = nil + obj = ( ptr ? ptr.to_object : nil ) + if obj.nil? + obj = Hash.new + ptr.set_object obj + end + obj + end + + def bind_blob( stmt, index, value ) + s = value.to_s + API.sqlite3_bind_blob( stmt, index, s, s.length, TRANSIENT ) + end + + def bind_text( stmt, index, value, utf16=false ) + s = value.to_s + method = ( utf16 ? :sqlite3_bind_text16 : :sqlite3_bind_text ) + API.send( method, stmt, index, s, s.length, TRANSIENT ) + end + + def column_text( stmt, column ) + result = API.sqlite3_column_text( stmt, column ) + result ? result.to_s : nil + end + + def column_name( stmt, column ) + result = API.sqlite3_column_name( stmt, column ) + result ? result.to_s : nil + end + + def column_decltype( stmt, column ) + result = API.sqlite3_column_decltype( stmt, column ) + result ? result.to_s : nil + end + + def self.api_delegate( name ) + define_method( name ) { |*args| API.send( "sqlite3_#{name}", *args ) } + end + + api_delegate :aggregate_count + api_delegate :bind_double + api_delegate :bind_int + api_delegate :bind_null + api_delegate :bind_parameter_index + api_delegate :bind_parameter_name + api_delegate :busy_timeout + api_delegate :changes + api_delegate :close + api_delegate :column_bytes + api_delegate :column_bytes16 + api_delegate :column_count + api_delegate :column_double + api_delegate :column_int + api_delegate :column_int64 + api_delegate :column_type + api_delegate :data_count + api_delegate :errcode + api_delegate :finalize + api_delegate :interrupt + api_delegate :last_insert_rowid + api_delegate :libversion + api_delegate :reset + api_delegate :result_error + api_delegate :step + api_delegate :total_changes + api_delegate :value_bytes + api_delegate :value_bytes16 + api_delegate :value_double + api_delegate :value_int + api_delegate :value_int64 + api_delegate :value_type + + # ==== EXPERIMENTAL ==== + if defined?( EXPERIMENTAL_API ) && EXPERIMENTAL_API + def progress_handler( db, n, data=nil, &block ) + @progress_handler = block + + unless @progress_handler_callback + @progress_handler_callback = ::DL.callback( "IP" ) do |cookie| + @progress_handler.call( cookie ) + end + end + + API.sqlite3_progress_handler( db, n, block&&@progress_handler_callback, + data ) + end + + def commit_hook( db, data=nil, &block ) + @commit_hook_handler = block + + unless @commit_hook_handler_callback + @commit_hook_handler_callback = ::DL.callback( "IP" ) do |cookie| + @commit_hook_handler.call( cookie ) + end + end + + API.sqlite3_commit_hook( db, block&&@commit_hook_handler_callback, + data ) + end + end + + private + + def utf16_length(ptr) + len = 0 + loop do + break if ptr[len,1] == "\0" + len += 2 + end + len + end + + end + +end ; end ; end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/driver/native/driver.rb b/vendor/plugins/sqlite3-ruby/sqlite3/driver/native/driver.rb new file mode 100644 index 00000000..cef0cdce --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/driver/native/driver.rb @@ -0,0 +1,243 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3_api' + +module SQLite3 ; module Driver ; module Native + + class Driver + + def initialize + @callback_data = Hash.new + @authorizer = Hash.new + @busy_handler = Hash.new + @trace = Hash.new + end + + def complete?( sql, utf16=false ) + API.send( utf16 ? :sqlite3_complete16 : :sqlite3_complete, sql ) != 0 + end + + def busy_handler( db, data=nil, &block ) + if block + cb = API::CallbackData.new + cb.proc = block + cb.data = data + result = API.sqlite3_busy_handler( db, API::Sqlite3_ruby_busy_handler, cb ) + # Reference the Callback object so that + # it is not deleted by the GC + @busy_handler[db] = cb + else + # Unreference the callback *after* having removed it + # from sqlite + result = API.sqlite3_busy_handler( db, nil, nil ) + @busy_handler.delete(db) + end + + result + end + + def set_authorizer( db, data=nil, &block ) + if block + cb = API::CallbackData.new + cb.proc = block + cb.data = data + result = API.sqlite3_set_authorizer( db, API::Sqlite3_ruby_authorizer, cb ) + @authorizer[db] = cb # see comments in busy_handler + else + result = API.sqlite3_set_authorizer( db, nil, nil ) + @authorizer.delete(db) # see comments in busy_handler + end + + result + end + + def trace( db, data=nil, &block ) + if block + cb = API::CallbackData.new + cb.proc = block + cb.data = data + result = API.sqlite3_trace( db, API::Sqlite3_ruby_trace, cb ) + @trace[db] = cb # see comments in busy_handler + else + result = API.sqlite3_trace( db, nil, nil ) + @trace.delete(db) # see comments in busy_handler + end + + result + end + + def open( filename, utf16=false ) + API.send( utf16 ? :sqlite3_open16 : :sqlite3_open, filename ) + end + + def errmsg( db, utf16=false ) + API.send( utf16 ? :sqlite3_errmsg16 : :sqlite3_errmsg, db ) + end + + def prepare( db, sql, utf16=false ) + API.send( ( utf16 ? :sqlite3_prepare16 : :sqlite3_prepare ), + db, sql ) + end + + def bind_text( stmt, index, value, utf16=false ) + API.send( ( utf16 ? :sqlite3_bind_text16 : :sqlite3_bind_text ), + stmt, index, value.to_s ) + end + + def column_name( stmt, index, utf16=false ) + API.send( ( utf16 ? :sqlite3_column_name16 : :sqlite3_column_name ), + stmt, index ) + end + + def column_decltype( stmt, index, utf16=false ) + API.send( + ( utf16 ? :sqlite3_column_decltype16 : :sqlite3_column_decltype ), + stmt, index ) + end + + def column_text( stmt, index, utf16=false ) + API.send( ( utf16 ? :sqlite3_column_text16 : :sqlite3_column_text ), + stmt, index ) + end + + def create_function( db, name, args, text, cookie, func, step, final ) + if func || ( step && final ) + cb = API::CallbackData.new + cb.proc = cb.proc2 = nil + cb.data = cookie + end + + if func + cb.proc = func + + func = API::Sqlite3_ruby_function_step + step = final = nil + elsif step && final + cb.proc = step + cb.proc2 = final + + func = nil + step = API::Sqlite3_ruby_function_step + final = API::Sqlite3_ruby_function_final + end + + result = API.sqlite3_create_function( db, name, args, text, cb, func, step, final ) + + # see comments in busy_handler + if cb + @callback_data[ name ] = cb + else + @callback_data.delete( name ) + end + + return result + end + + def value_text( value, utf16=false ) + method = case utf16 + when nil, false then :sqlite3_value_text + when :le then :sqlite3_value_text16le + when :be then :sqlite3_value_text16be + else :sqlite3_value_text16 + end + + API.send( method, value ) + end + + def result_text( context, result, utf16=false ) + method = case utf16 + when nil, false then :sqlite3_result_text + when :le then :sqlite3_result_text16le + when :be then :sqlite3_result_text16be + else :sqlite3_result_text16 + end + + API.send( method, context, result.to_s ) + end + + def result_error( context, value, utf16=false ) + API.send( ( utf16 ? :sqlite3_result_error16 : :sqlite3_result_error ), + context, value ) + end + + def self.api_delegate( name ) + define_method( name ) { |*args| API.send( "sqlite3_#{name}", *args ) } + end + + api_delegate :libversion + api_delegate :close + api_delegate :last_insert_rowid + api_delegate :changes + api_delegate :total_changes + api_delegate :interrupt + api_delegate :busy_timeout + api_delegate :errcode + api_delegate :bind_blob + api_delegate :bind_double + api_delegate :bind_int + api_delegate :bind_int64 + api_delegate :bind_null + api_delegate :bind_parameter_count + api_delegate :bind_parameter_name + api_delegate :bind_parameter_index + api_delegate :column_count + api_delegate :step + api_delegate :data_count + api_delegate :column_blob + api_delegate :column_bytes + api_delegate :column_bytes16 + api_delegate :column_double + api_delegate :column_int + api_delegate :column_int64 + api_delegate :column_type + api_delegate :finalize + api_delegate :reset + api_delegate :aggregate_count + api_delegate :value_blob + api_delegate :value_bytes + api_delegate :value_bytes16 + api_delegate :value_double + api_delegate :value_int + api_delegate :value_int64 + api_delegate :value_type + api_delegate :result_blob + api_delegate :result_double + api_delegate :result_int + api_delegate :result_int64 + api_delegate :result_null + api_delegate :result_value + api_delegate :aggregate_context + + end + +end ; end ; end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/errors.rb b/vendor/plugins/sqlite3-ruby/sqlite3/errors.rb new file mode 100644 index 00000000..f83adc57 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/errors.rb @@ -0,0 +1,100 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/constants' + +module SQLite3 + + class Exception < ::Exception + @code = 0 + + # The numeric error code that this exception represents. + def self.code + @code + end + + # A convenience for accessing the error code for this exception. + def code + self.class.code + end + end + + class SQLException < Exception; end + class InternalException < Exception; end + class PermissionException < Exception; end + class AbortException < Exception; end + class BusyException < Exception; end + class LockedException < Exception; end + class MemoryException < Exception; end + class ReadOnlyException < Exception; end + class InterruptException < Exception; end + class IOException < Exception; end + class CorruptException < Exception; end + class NotFoundException < Exception; end + class FullException < Exception; end + class CantOpenException < Exception; end + class ProtocolException < Exception; end + class EmptyException < Exception; end + class SchemaChangedException < Exception; end + class TooBigException < Exception; end + class ConstraintException < Exception; end + class MismatchException < Exception; end + class MisuseException < Exception; end + class UnsupportedException < Exception; end + class AuthorizationException < Exception; end + class FormatException < Exception; end + class RangeException < Exception; end + class NotADatabaseException < Exception; end + + EXCEPTIONS = [ + nil, + SQLException, InternalException, PermissionException, + AbortException, BusyException, LockedException, MemoryException, + ReadOnlyException, InterruptException, IOException, CorruptException, + NotFoundException, FullException, CantOpenException, ProtocolException, + EmptyException, SchemaChangedException, TooBigException, + ConstraintException, MismatchException, MisuseException, + UnsupportedException, AuthorizationException, FormatException, + RangeException, NotADatabaseException + ].each_with_index { |e,i| e.instance_variable_set( :@code, i ) if e } + + module Error + def check( result, db=nil, msg=nil ) + unless result == Constants::ErrorCode::OK + msg = ( msg ? msg + ": " : "" ) + db.errmsg if db + raise(( EXCEPTIONS[result] || SQLite3::Exception ), msg) + end + end + module_function :check + end + +end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/pragmas.rb b/vendor/plugins/sqlite3-ruby/sqlite3/pragmas.rb new file mode 100644 index 00000000..72473871 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/pragmas.rb @@ -0,0 +1,254 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/errors' + +module SQLite3 + + # This module is intended for inclusion solely by the Database class. It + # defines convenience methods for the various pragmas supported by SQLite3. + # + # For a detailed description of these pragmas, see the SQLite3 documentation + # at http://sqlite.org/pragma.html. + module Pragmas + + # Returns +true+ or +false+ depending on the value of the named pragma. + def get_boolean_pragma( name ) + get_first_value( "PRAGMA #{name}" ) != "0" + end + private :get_boolean_pragma + + # Sets the given pragma to the given boolean value. The value itself + # may be +true+ or +false+, or any other commonly used string or + # integer that represents truth. + def set_boolean_pragma( name, mode ) + case mode + when String + case mode.downcase + when "on", "yes", "true", "y", "t": mode = "'ON'" + when "off", "no", "false", "n", "f": mode = "'OFF'" + else + raise Exception, + "unrecognized pragma parameter #{mode.inspect}" + end + when true, 1 + mode = "ON" + when false, 0, nil + mode = "OFF" + else + raise Exception, + "unrecognized pragma parameter #{mode.inspect}" + end + + execute( "PRAGMA #{name}=#{mode}" ) + end + private :set_boolean_pragma + + # Requests the given pragma (and parameters), and if the block is given, + # each row of the result set will be yielded to it. Otherwise, the results + # are returned as an array. + def get_query_pragma( name, *parms, &block ) # :yields: row + if parms.empty? + execute( "PRAGMA #{name}", &block ) + else + args = "'" + parms.join("','") + "'" + execute( "PRAGMA #{name}( #{args} )", &block ) + end + end + private :get_query_pragma + + # Return the value of the given pragma. + def get_enum_pragma( name ) + get_first_value( "PRAGMA #{name}" ) + end + private :get_enum_pragma + + # Set the value of the given pragma to +mode+. The +mode+ parameter must + # conform to one of the values in the given +enum+ array. Each entry in + # the array is another array comprised of elements in the enumeration that + # have duplicate values. See #synchronous, #default_synchronous, + # #temp_store, and #default_temp_store for usage examples. + def set_enum_pragma( name, mode, enums ) + match = enums.find { |p| p.find { |i| i.to_s.downcase == mode.to_s.downcase } } + raise Exception, + "unrecognized #{name} #{mode.inspect}" unless match + execute( "PRAGMA #{name}='#{match.first.upcase}'" ) + end + private :set_enum_pragma + + # Returns the value of the given pragma as an integer. + def get_int_pragma( name ) + get_first_value( "PRAGMA #{name}" ).to_i + end + private :get_int_pragma + + # Set the value of the given pragma to the integer value of the +value+ + # parameter. + def set_int_pragma( name, value ) + execute( "PRAGMA #{name}=#{value.to_i}" ) + end + private :set_int_pragma + + # The enumeration of valid synchronous modes. + SYNCHRONOUS_MODES = [ [ 'full', 2 ], [ 'normal', 1 ], [ 'off', 0 ] ] + + # The enumeration of valid temp store modes. + TEMP_STORE_MODES = [ [ 'default', 0 ], [ 'file', 1 ], [ 'memory', 2 ] ] + + # Does an integrity check on the database. If the check fails, a + # SQLite3::Exception will be raised. Otherwise it + # returns silently. + def integrity_check + execute( "PRAGMA integrity_check" ) do |row| + raise Exception, row[0] if row[0] != "ok" + end + end + + def auto_vacuum + get_boolean_pragma "auto_vacuum" + end + + def auto_vacuum=( mode ) + set_boolean_pragma "auto_vacuum", mode + end + + def schema_cookie + get_int_pragma "schema_cookie" + end + + def schema_cookie=( cookie ) + set_int_pragma "schema_cookie", cookie + end + + def user_cookie + get_int_pragma "user_cookie" + end + + def user_cookie=( cookie ) + set_int_pragma "user_cookie", cookie + end + + def cache_size + get_int_pragma "cache_size" + end + + def cache_size=( size ) + set_int_pragma "cache_size", size + end + + def default_cache_size + get_int_pragma "default_cache_size" + end + + def default_cache_size=( size ) + set_int_pragma "default_cache_size", size + end + + def default_synchronous + get_enum_pragma "default_synchronous" + end + + def default_synchronous=( mode ) + set_enum_pragma "default_synchronous", mode, SYNCHRONOUS_MODES + end + + def synchronous + get_enum_pragma "synchronous" + end + + def synchronous=( mode ) + set_enum_pragma "synchronous", mode, SYNCHRONOUS_MODES + end + + def default_temp_store + get_enum_pragma "default_temp_store" + end + + def default_temp_store=( mode ) + set_enum_pragma "default_temp_store", mode, TEMP_STORE_MODES + end + + def temp_store + get_enum_pragma "temp_store" + end + + def temp_store=( mode ) + set_enum_pragma "temp_store", mode, TEMP_STORE_MODES + end + + def full_column_names + get_boolean_pragma "full_column_names" + end + + def full_column_names=( mode ) + set_boolean_pragma "full_column_names", mode + end + + def parser_trace + get_boolean_pragma "parser_trace" + end + + def parser_trace=( mode ) + set_boolean_pragma "parser_trace", mode + end + + def vdbe_trace + get_boolean_pragma "vdbe_trace" + end + + def vdbe_trace=( mode ) + set_boolean_pragma "vdbe_trace", mode + end + + def database_list( &block ) # :yields: row + get_query_pragma "database_list", &block + end + + def foreign_key_list( table, &block ) # :yields: row + get_query_pragma "foreign_key_list", table, &block + end + + def index_info( index, &block ) # :yields: row + get_query_pragma "index_info", index, &block + end + + def index_list( table, &block ) # :yields: row + get_query_pragma "index_list", table, &block + end + + def table_info( table, &block ) # :yields: row + get_query_pragma "table_info", table, &block + end + + end + +end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/resultset.rb b/vendor/plugins/sqlite3-ruby/sqlite3/resultset.rb new file mode 100644 index 00000000..ed2cb582 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/resultset.rb @@ -0,0 +1,190 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/constants' +require 'sqlite3/errors' + +module SQLite3 + + # The ResultSet object encapsulates the enumerability of a query's output. + # It is a simple cursor over the data that the query returns. It will + # very rarely (if ever) be instantiated directly. Instead, client's should + # obtain a ResultSet instance via Statement#execute. + class ResultSet + include Enumerable + + # A trivial module for adding a +types+ accessor to an object. + module TypesContainer + attr_accessor :types + end + + # A trivial module for adding a +fields+ accessor to an object. + module FieldsContainer + attr_accessor :fields + end + + # Create a new ResultSet attached to the given database, using the + # given sql text. + def initialize( db, stmt ) + @db = db + @driver = @db.driver + @stmt = stmt + commence + end + + # A convenience method for compiling the virtual machine and stepping + # to the first row of the result set. + def commence + result = @driver.step( @stmt.handle ) + check result + @first_row = true + end + private :commence + + def check( result ) + @eof = ( result == Constants::ErrorCode::DONE ) + found = ( result == Constants::ErrorCode::ROW ) + Error.check( result, @db ) unless @eof || found + end + private :check + + # Reset the cursor, so that a result set which has reached end-of-file + # can be rewound and reiterated. + def reset( *bind_params ) + @stmt.must_be_open! + @stmt.reset!(false) + @driver.reset( @stmt.handle ) + @stmt.bind_params( *bind_params ) + @eof = false + commence + end + + # Query whether the cursor has reached the end of the result set or not. + def eof? + @eof + end + + # Obtain the next row from the cursor. If there are no more rows to be + # had, this will return +nil+. If type translation is active on the + # corresponding database, the values in the row will be translated + # according to their types. + # + # The returned value will be an array, unless Database#results_as_hash has + # been set to +true+, in which case the returned value will be a hash. + # + # For arrays, the column names are accessible via the +fields+ property, + # and the column types are accessible via the +types+ property. + # + # For hashes, the column names are the keys of the hash, and the column + # types are accessible via the +types+ property. + def next + return nil if @eof + + @stmt.must_be_open! + + unless @first_row + result = @driver.step( @stmt.handle ) + check result + end + + @first_row = false + + unless @eof + row = [] + @driver.data_count( @stmt.handle ).times do |column| + case @driver.column_type( @stmt.handle, column ) + when Constants::ColumnType::NULL then + row << nil + when Constants::ColumnType::BLOB then + row << @driver.column_blob( @stmt.handle, column ) + else + row << @driver.column_text( @stmt.handle, column ) + end + end + + if @db.type_translation + row = @stmt.types.zip( row ).map do |type, value| + @db.translator.translate( type, value ) + end + end + + if @db.results_as_hash + new_row = Hash[ *( @stmt.columns.zip( row ).flatten ) ] + row.each_with_index { |value,idx| new_row[idx] = value } + row = new_row + else + row.extend FieldsContainer unless row.respond_to?(:fields) + row.fields = @stmt.columns + end + + row.extend TypesContainer + row.types = @stmt.types + + return row + end + + nil + end + + # Required by the Enumerable mixin. Provides an internal iterator over the + # rows of the result set. + def each + while row=self.next + yield row + end + end + + # Closes the statement that spawned this result set. + # Use with caution! Closing a result set will automatically + # close any other result sets that were spawned from the same statement. + def close + @stmt.close + end + + # Queries whether the underlying statement has been closed or not. + def closed? + @stmt.closed? + end + + # Returns the types of the columns returned by this result set. + def types + @stmt.types + end + + # Returns the names of the columns returned by this result set. + def columns + @stmt.columns + end + + end + +end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/statement.rb b/vendor/plugins/sqlite3-ruby/sqlite3/statement.rb new file mode 100644 index 00000000..09f24d91 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/statement.rb @@ -0,0 +1,258 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/errors' +require 'sqlite3/resultset' + +class String + def to_blob + SQLite3::Blob.new( self ) + end +end + +module SQLite3 + + # A class for differentiating between strings and blobs, when binding them + # into statements. + class Blob < String; end + + # A statement represents a prepared-but-unexecuted SQL query. It will rarely + # (if ever) be instantiated directly by a client, and is most often obtained + # via the Database#prepare method. + class Statement + + # This is any text that followed the first valid SQL statement in the text + # with which the statement was initialized. If there was no trailing text, + # this will be the empty string. + attr_reader :remainder + + # The underlying opaque handle used to access the SQLite @driver. + attr_reader :handle + + # Create a new statement attached to the given Database instance, and which + # encapsulates the given SQL text. If the text contains more than one + # statement (i.e., separated by semicolons), then the #remainder property + # will be set to the trailing text. + def initialize( db, sql, utf16=false ) + @db = db + @driver = @db.driver + @closed = false + @results = @columns = nil + result, @handle, @remainder = @driver.prepare( @db.handle, sql ) + Error.check( result, @db ) + end + + # Closes the statement by finalizing the underlying statement + # handle. The statement must not be used after being closed. + def close + must_be_open! + @closed = true + @driver.finalize( @handle ) + end + + # Returns true if the underlying statement has been closed. + def closed? + @closed + end + + # Binds the given variables to the corresponding placeholders in the SQL + # text. + # + # See Database#execute for a description of the valid placeholder + # syntaxes. + # + # Example: + # + # stmt = db.prepare( "select * from table where a=? and b=?" ) + # stmt.bind_params( 15, "hello" ) + # + # See also #execute, #bind_param, Statement#bind_param, and + # Statement#bind_params. + def bind_params( *bind_vars ) + index = 1 + bind_vars.flatten.each do |var| + if Hash === var + var.each { |key, val| bind_param key, val } + else + bind_param index, var + index += 1 + end + end + end + + # Binds value to the named (or positional) placeholder. If +param+ is a + # Fixnum, it is treated as an index for a positional placeholder. + # Otherwise it is used as the name of the placeholder to bind to. + # + # See also #bind_params. + def bind_param( param, value ) + must_be_open! + reset! if active? + if Fixnum === param + case value + when Bignum then + @driver.bind_int64( @handle, param, value ) + when Integer then + @driver.bind_int( @handle, param, value ) + when Numeric then + @driver.bind_double( @handle, param, value.to_f ) + when Blob then + @driver.bind_blob( @handle, param, value ) + when nil then + @driver.bind_null( @handle, param ) + else + @driver.bind_text( @handle, param, value ) + end + else + param = param.to_s + param = ":#{param}" unless param[0] == ?: + index = @driver.bind_parameter_index( @handle, param ) + raise Exception, "no such bind parameter '#{param}'" if index == 0 + bind_param index, value + end + end + + # Execute the statement. This creates a new ResultSet object for the + # statement's virtual machine. If a block was given, the new ResultSet will + # be yielded to it; otherwise, the ResultSet will be returned. + # + # Any parameters will be bound to the statement using #bind_params. + # + # Example: + # + # stmt = db.prepare( "select * from table" ) + # stmt.execute do |result| + # ... + # end + # + # See also #bind_params, #execute!. + def execute( *bind_vars ) + must_be_open! + reset! if active? + + bind_params(*bind_vars) unless bind_vars.empty? + @results = ResultSet.new( @db, self ) + + if block_given? + yield @results + else + return @results + end + end + + # Execute the statement. If no block was given, this returns an array of + # rows returned by executing the statement. Otherwise, each row will be + # yielded to the block. + # + # Any parameters will be bound to the statement using #bind_params. + # + # Example: + # + # stmt = db.prepare( "select * from table" ) + # stmt.execute! do |row| + # ... + # end + # + # See also #bind_params, #execute. + def execute!( *bind_vars ) + result = execute( *bind_vars ) + rows = [] unless block_given? + while row = result.next + if block_given? + yield row + else + rows << row + end + end + rows + end + + # Resets the statement. This is typically done internally, though it might + # occassionally be necessary to manually reset the statement. + def reset!(clear_result=true) + @driver.reset(@handle) + @results = nil if clear_result + end + + # Returns true if the statement is currently active, meaning it has an + # open result set. + def active? + not @results.nil? + end + + # Return an array of the column names for this statement. Note that this + # may execute the statement in order to obtain the metadata; this makes it + # a (potentially) expensive operation. + def columns + get_metadata unless @columns + return @columns + end + + # Return an array of the data types for each column in this statement. Note + # that this may execute the statement in order to obtain the metadata; this + # makes it a (potentially) expensive operation. + def types + get_metadata unless @types + return @types + end + + # A convenience method for obtaining the metadata about the query. Note + # that this will actually execute the SQL, which means it can be a + # (potentially) expensive operation. + def get_metadata + must_be_open! + + @columns = [] + @types = [] + + column_count = @driver.column_count( @handle ) + column_count.times do |column| + @columns << @driver.column_name( @handle, column ) + @types << @driver.column_decltype( @handle, column ) + end + + @columns.freeze + @types.freeze + end + private :get_metadata + + # Performs a sanity check to ensure that the statement is not + # closed. If it is, an exception is raised. + def must_be_open! # :nodoc: + if @closed + raise SQLite3::Exception, "cannot use a closed statement" + end + end + + end + +end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/translator.rb b/vendor/plugins/sqlite3-ruby/sqlite3/translator.rb new file mode 100644 index 00000000..14d28b6c --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/translator.rb @@ -0,0 +1,136 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'time' + +module SQLite3 + + # The Translator class encapsulates the logic and callbacks necessary for + # converting string data to a value of some specified type. Every Database + # instance may have a Translator instance, in order to assist in type + # translation (Database#type_translation). + # + # Further, applications may define their own custom type translation logic + # by registering translator blocks with the corresponding database's + # translator instance (Database#translator). + class Translator + + # Create a new Translator instance. It will be preinitialized with default + # translators for most SQL data types. + def initialize + @translators = Hash.new( proc { |type,value| value } ) + register_default_translators + end + + # Add a new translator block, which will be invoked to process type + # translations to the given type. The type should be an SQL datatype, and + # may include parentheses (i.e., "VARCHAR(30)"). However, any parenthetical + # information is stripped off and discarded, so type translation decisions + # are made solely on the "base" type name. + # + # The translator block itself should accept two parameters, "type" and + # "value". In this case, the "type" is the full type name (including + # parentheses), so the block itself may include logic for changing how a + # type is translated based on the additional data. The "value" parameter + # is the (string) data to convert. + # + # The block should return the translated value. + def add_translator( type, &block ) # :yields: type, value + @translators[ type_name( type ) ] = block + end + + # Translate the given string value to a value of the given type. In the + # absense of an installed translator block for the given type, the value + # itself is always returned. Further, +nil+ values are never translated, + # and are always passed straight through regardless of the type parameter. + def translate( type, value ) + unless value.nil? + @translators[ type_name( type ) ].call( type, value ) + end + end + + # A convenience method for working with type names. This returns the "base" + # type name, without any parenthetical data. + def type_name( type ) + return "" if type.nil? + type = $1 if type =~ /^(.*?)\(/ + type.upcase + end + private :type_name + + # Register the default translators for the current Translator instance. + # This includes translators for most major SQL data types. + def register_default_translators + [ "date", + "datetime", + "time" ].each { |type| add_translator( type ) { |t,v| Time.parse( v ) } } + + [ "decimal", + "float", + "numeric", + "double", + "real", + "dec", + "fixed" ].each { |type| add_translator( type ) { |t,v| v.to_f } } + + [ "integer", + "smallint", + "mediumint", + "int", + "bigint" ].each { |type| add_translator( type ) { |t,v| v.to_i } } + + [ "bit", + "bool", + "boolean" ].each do |type| + add_translator( type ) do |t,v| + !( v.strip.gsub(/00+/,"0") == "0" || + v.downcase == "false" || + v.downcase == "f" || + v.downcase == "no" || + v.downcase == "n" ) + end + end + + add_translator( "timestamp" ) { |type, value| Time.at( value.to_i ) } + add_translator( "tinyint" ) do |type, value| + if type =~ /\(\s*1\s*\)/ + value.to_i == 1 + else + value.to_i + end + end + end + private :register_default_translators + + end + +end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/value.rb b/vendor/plugins/sqlite3-ruby/sqlite3/value.rb new file mode 100644 index 00000000..fb763763 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/value.rb @@ -0,0 +1,89 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/constants' + +module SQLite3 + + class Value + attr_reader :handle + + def initialize( db, handle ) + @driver = db.driver + @handle = handle + end + + def null? + type == :null + end + + def to_blob + @driver.value_blob( @handle ) + end + + def length( utf16=false ) + if utf16 + @driver.value_bytes16( @handle ) + else + @driver.value_bytes( @handle ) + end + end + + def to_f + @driver.value_double( @handle ) + end + + def to_i + @driver.value_int( @handle ) + end + + def to_int64 + @driver.value_int64( @handle ) + end + + def to_s( utf16=false ) + @driver.value_text( @handle, utf16 ) + end + + def type + case @driver.value_type( @handle ) + when Constants::ColumnType::INTEGER then :int + when Constants::ColumnType::FLOAT then :float + when Constants::ColumnType::TEXT then :text + when Constants::ColumnType::BLOB then :blob + when Constants::ColumnType::NULL then :null + end + end + + end + +end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/version.rb b/vendor/plugins/sqlite3-ruby/sqlite3/version.rb new file mode 100644 index 00000000..12e678b0 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/version.rb @@ -0,0 +1,45 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +module SQLite3 + + module Version + + MAJOR = 1 + MINOR = 2 + TINY = 0 + + STRING = [ MAJOR, MINOR, TINY ].join( "." ) + + end + +end diff --git a/vendor/rails/actionmailer/CHANGELOG b/vendor/rails/actionmailer/CHANGELOG new file mode 100644 index 00000000..34d1ac4b --- /dev/null +++ b/vendor/rails/actionmailer/CHANGELOG @@ -0,0 +1,257 @@ +*1.2.5* (August 10th, 2006) + +* Depend on Action Pack 1.12.5 + + +*1.2.4* (August 8th, 2006) + +* Backport of documentation enhancements. [Kevin Clark, Marcel Molina Jr] + +* Correct spurious documentation example code which results in a SyntaxError. [Marcel Molina Jr.] + +* Mailer template root applies to a class and its subclasses rather than acting globally. #5555 [somekool@gmail.com] + + +*1.2.3* (June 29th, 2006) + +* Depend on Action Pack 1.12.3 + + +*1.2.2* (June 27th, 2006) + +* Depend on Action Pack 1.12.2 + + +*1.2.1* (April 6th, 2005) + +* Be part of Rails 1.1.1 + + +*1.2.0* (March 27th, 2005) + +* Nil charset caused subject line to be improperly quoted in implicitly multipart messages #2662 [ehalvorsen+rails@runbox.com] + +* Parse content-type apart before using it so that sub-parts of the header can be set correctly #2918 [Jamis Buck] + +* Make custom headers work in subparts #4034 [elan@bluemandrill.com] + +* Template paths with dot chars in them no longer mess up implicit template selection for multipart messages #3332 [Chad Fowler] + +* Make sure anything with content-disposition of "attachment" is passed to the attachment presenter when parsing an email body [Jamis Buck] + +* Make sure TMail#attachments includes anything with content-disposition of "attachment", regardless of content-type [Jamis Buck] + + +*1.1.5* (December 13th, 2005) + +* Become part of Rails 1.0 + + +*1.1.4* (December 7th, 2005) + +* Rename Version constant to VERSION. #2802 [Marcel Molina Jr.] + +* Stricter matching for implicitly multipart filenames excludes files ending in unsupported extensions (such as foo.rhtml.bak) and without a two-part content type (such as foo.text.rhtml or foo.text.really.plain.rhtml). #2398 [Dave Burt , Jeremy Kemper] + + +*1.1.3* (November 7th, 2005) + +* Allow Mailers to have custom initialize methods that set default instance variables for all mail actions #2563 [mrj@bigpond.net.au] + + +*1.1.2* (October 26th, 2005) + +* Upgraded to Action Pack 1.10.2 + + +*1.1.1* (October 19th, 2005) + +* Upgraded to Action Pack 1.10.1 + + +*1.1.0* (October 16th, 2005) + +* Update and extend documentation (rdoc) + +* Minero Aoki made TMail available to Rails/ActionMailer under the MIT license (instead of LGPL) [RubyConf '05] + +* Austin Ziegler made Text::Simple available to Rails/ActionMailer under a MIT-like licens [See rails ML, subject "Text::Format Licence Exception" on Oct 15, 2005] + +* Fix vendor require paths to prevent files being required twice + +* Don't add charset to content-type header for a part that contains subparts (for AOL compatibility) #2013 [John Long] + +* Preserve underscores when unquoting message bodies #1930 + +* Encode multibyte characters correctly #1894 + +* Multipart messages specify a MIME-Version header automatically #2003 [John Long] + +* Add a unified render method to ActionMailer (delegates to ActionView::Base#render) + +* Move mailer initialization to a separate (overridable) method, so that subclasses may alter the various defaults #1727 + +* Look at content-location header (if available) to determine filename of attachments #1670 + +* ActionMailer::Base.deliver(email) had been accidentally removed, but was documented in the Rails book #1849 + +* Fix problem with sendmail delivery where headers should be delimited by \n characters instead of \r\n, which confuses some mail readers #1742 [Kent Sibilev] + + +*1.0.1* (11 July, 2005) + +* Bind to Action Pack 1.9.1 + + +*1.0.0* (6 July, 2005) + +* Avoid adding nil header values #1392 + +* Better multipart support with implicit multipart/alternative and sorting of subparts [John Long] + +* Allow for nested parts in multipart mails #1570 [Flurin Egger] + +* Normalize line endings in outgoing mail bodies to "\n" #1536 [John Long] + +* Allow template to be explicitly specified #1448 [tuxie@dekadance.se] + +* Allow specific "multipart/xxx" content-type to be set on multipart messages #1412 [Flurin Egger] + +* Unquoted @ characters in headers are now accepted in spite of RFC 822 #1206 + +* Helper support (borrowed from ActionPack) + +* Silently ignore Errno::EINVAL errors when converting text. + +* Don't cause an error when parsing an encoded attachment name #1340 [lon@speedymac.com] + +* Nested multipart message parts are correctly processed in TMail::Mail#body + +* BCC headers are removed when sending via SMTP #1402 + +* Added 'content_type' accessor, to allow content type to be set on a per-message basis. content_type defaults to "text/plain". + +* Silently ignore Iconv::IllegalSequence errors when converting text #1341 [lon@speedymac.com] + +* Support attachments and multipart messages. + +* Added new accessors for the various mail properties. + +* Fix to only perform the charset conversion if a 'from' and a 'to' charset are given (make no assumptions about what the charset was) #1276 [Jamis Buck] + +* Fix attachments and content-type problems #1276 [Jamis Buck] + +* Fixed the TMail#body method to look at the content-transfer-encoding header and unquote the body according to the rules it specifies #1265 [Jamis Buck] + +* Added unquoting even if the iconv lib can't be loaded--in that case, only the charset conversion is skipped #1265 [Jamis Buck] + +* Added automatic decoding of base64 bodies #1214 [Jamis Buck] + +* Added that delivery errors are caught in a way so the mail is still returned whether the delivery was successful or not + +* Fixed that email address like "Jamis Buck, M.D." would cause the quoter to generate emails resulting in "bad address" errors from the mail server #1220 [Jamis Buck] + + +*0.9.1* (20th April, 2005) + +* Depend on Action Pack 1.8.1 + + +*0.9.0* (19th April, 2005) + +* Added that deliver_* will now return the email that was sent + +* Added that quoting to UTF-8 only happens if the characters used are in that range #955 [Jamis Buck] + +* Fixed quoting for all address headers, not just to #955 [Jamis Buck] + +* Fixed unquoting of emails that doesn't have an explicit charset #1036 [wolfgang@stufenlos.net] + + +*0.8.1* (27th March, 2005) + +* Fixed that if charset was found that the end of a mime part declaration TMail would throw an error #919 [lon@speedymac.com] + +* Fixed that TMail::Unquoter would fail to recognize quoting method if it was in lowercase #919 [lon@speedymac.com] + +* Fixed that TMail::Encoder would fail when it attempts to parse e-mail addresses which are encoded using something other than the messages encoding method #919 [lon@speedymac.com] + +* Added rescue for missing iconv library and throws warnings if subject/body is called on a TMail object without it instead + + +*0.8.0* (22th March, 2005) + +* Added framework support for processing incoming emails with an Action Mailer class. See example in README. + + +*0.7.1* (7th March, 2005) + +* Bind to newest Action Pack (1.5.1) + + +*0.7.0* (24th February, 2005) + +* Added support for charsets for both subject and body. The default charset is now UTF-8 #673 [Jamis Buck]. Examples: + + def iso_charset(recipient) + @recipients = recipient + @subject = "testing iso charsets" + @from = "system@loudthinking.com" + @body = "Nothing to see here." + @charset = "iso-8859-1" + end + + def unencoded_subject(recipient) + @recipients = recipient + @subject = "testing unencoded subject" + @from = "system@loudthinking.com" + @body = "Nothing to see here." + @encode_subject = false + @charset = "iso-8859-1" + end + + +*0.6.1* (January 18th, 2005) + +* Fixed sending of emails to use Tmail#from not the deprecated Tmail#from_address + + +*0.6* (January 17th, 2005) + +* Fixed that bcc and cc should be settable through @bcc and @cc -- not just @headers["Bcc"] and @headers["Cc"] #453 [Eric Hodel] + +* Fixed Action Mailer to be "warnings safe" so you can run with ruby -w and not get framework warnings #453 [Eric Hodel] + + +*0.5* + +* Added access to custom headers, like cc, bcc, and reply-to #268 [Andreas Schwarz]. Example: + + def post_notification(recipients, post) + @recipients = recipients + @from = post.author.email_address_with_name + @headers["bcc"] = SYSTEM_ADMINISTRATOR_EMAIL + @headers["reply-to"] = "notifications@example.com" + @subject = "[#{post.account.name} #{post.title}]" + @body["post"] = post + end + +*0.4* (5) + +* Consolidated the server configuration options into Base#server_settings= and expanded that with controls for authentication and more [Marten] + NOTE: This is an API change that could potentially break your application if you used the old application form. Please do change! + +* Added Base#deliveries as an accessor for an array of emails sent out through that ActionMailer class when using the :test delivery option. [Jeremy Kemper] + +* Added Base#perform_deliveries= which can be set to false to turn off the actual delivery of the email through smtp or sendmail. + This is especially useful for functional testing that shouldn't send off real emails, but still trigger delivery_* methods. + +* Added option to specify delivery method with Base#delivery_method=. Default is :smtp and :sendmail is currently the only other option. + Sendmail is assumed to be present at "/usr/sbin/sendmail" if that option is used. [Kent Sibilev] + +* Dropped "include TMail" as it added to much baggage into the default namespace (like Version) [Chad Fowler] + + +*0.3* + +* First release diff --git a/vendor/rails/actionmailer/MIT-LICENSE b/vendor/rails/actionmailer/MIT-LICENSE new file mode 100644 index 00000000..26f55e77 --- /dev/null +++ b/vendor/rails/actionmailer/MIT-LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2004 David Heinemeier Hansson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/vendor/rails/actionmailer/README b/vendor/rails/actionmailer/README new file mode 100755 index 00000000..8c85e1ae --- /dev/null +++ b/vendor/rails/actionmailer/README @@ -0,0 +1,148 @@ += Action Mailer -- Easy email delivery and testing + +Action Mailer is a framework for designing email-service layers. These layers +are used to consolidate code for sending out forgotten passwords, welcoming +wishes on signup, invoices for billing, and any other use case that requires +a written notification to either a person or another system. + +Additionally, an Action Mailer class can be used to process incoming email, +such as allowing a weblog to accept new posts from an email (which could even +have been sent from a phone). + +== Sending emails + +The framework works by setting up all the email details, except the body, +in methods on the service layer. Subject, recipients, sender, and timestamp +are all set up this way. An example of such a method: + + def signed_up(recipient) + recipients recipient + subject "[Signed up] Welcome #{recipient}" + from "system@loudthinking.com" + + body(:recipient => recipient) + end + +The body of the email is created by using an Action View template (regular +ERb) that has the content of the body hash parameter available as instance variables. +So the corresponding body template for the method above could look like this: + + Hello there, + + Mr. <%= @recipient %> + +And if the recipient was given as "david@loudthinking.com", the email +generated would look like this: + + Date: Sun, 12 Dec 2004 00:00:00 +0100 + From: system@loudthinking.com + To: david@loudthinking.com + Subject: [Signed up] Welcome david@loudthinking.com + + Hello there, + + Mr. david@loudthinking.com + +You never actually call the instance methods like signed_up directly. Instead, +you call class methods like deliver_* and create_* that are automatically +created for each instance method. So if the signed_up method sat on +ApplicationMailer, it would look like this: + + ApplicationMailer.create_signed_up("david@loudthinking.com") # => tmail object for testing + ApplicationMailer.deliver_signed_up("david@loudthinking.com") # sends the email + ApplicationMailer.new.signed_up("david@loudthinking.com") # won't work! + +== Receiving emails + +To receive emails, you need to implement a public instance method called receive that takes a +tmail object as its single parameter. The Action Mailer framework has a corresponding class method, +which is also called receive, that accepts a raw, unprocessed email as a string, which it then turns +into the tmail object and calls the receive instance method. + +Example: + + class Mailman < ActionMailer::Base + def receive(email) + page = Page.find_by_address(email.to.first) + page.emails.create( + :subject => email.subject, :body => email.body + ) + + if email.has_attachments? + for attachment in email.attachments + page.attachments.create({ + :file => attachment, :description => email.subject + }) + end + end + end + end + +This Mailman can be the target for Postfix. In Rails, you would use the runner like this: + + ./script/runner 'Mailman.receive(STDIN.read)' + +== Configuration + +The Base class has the full list of configuration options. Here's an example: + +ActionMailer::Base.server_settings = { + :address=>'smtp.yourserver.com', # default: localhost + :port=>'25', # default: 25 + :user_name=>'user', + :password=>'pass', + :authentication=>:plain # :plain, :login or :cram_md5 +} + +== Dependencies + +Action Mailer requires that the Action Pack is either available to be required immediately +or is accessible as a GEM. + + +== Bundled software + +* tmail 0.10.8 by Minero Aoki released under LGPL + Read more on http://i.loveruby.net/en/prog/tmail.html + +* Text::Format 0.63 by Austin Ziegler released under OpenSource + Read more on http://www.halostatue.ca/ruby/Text__Format.html + + +== Download + +The latest version of Action Mailer can be found at + +* http://rubyforge.org/project/showfiles.php?group_id=361 + +Documentation can be found at + +* http://actionmailer.rubyonrails.org + + +== Installation + +You can install Action Mailer with the following command. + + % [sudo] ruby install.rb + +from its distribution directory. + + +== License + +Action Mailer is released under the MIT license. + + +== Support + +The Action Mailer homepage is http://actionmailer.rubyonrails.org. You can find +the Action Mailer RubyForge page at http://rubyforge.org/projects/actionmailer. +And as Jim from Rake says: + + Feel free to submit commits or feature requests. If you send a patch, + remember to update the corresponding unit tests. If fact, I prefer + new feature to be submitted in the form of new unit tests. + +For other information, feel free to ask on the ruby-talk mailing list (which +is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com. diff --git a/vendor/rails/actionmailer/Rakefile b/vendor/rails/actionmailer/Rakefile new file mode 100755 index 00000000..6095c6dc --- /dev/null +++ b/vendor/rails/actionmailer/Rakefile @@ -0,0 +1,95 @@ +require 'rubygems' +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/contrib/rubyforgepublisher' +require File.join(File.dirname(__FILE__), 'lib', 'action_mailer', 'version') + +PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' +PKG_NAME = 'actionmailer' +PKG_VERSION = ActionMailer::VERSION::STRING + PKG_BUILD +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" + +RELEASE_NAME = "REL #{PKG_VERSION}" + +RUBY_FORGE_PROJECT = "actionmailer" +RUBY_FORGE_USER = "webster132" + +desc "Default Task" +task :default => [ :test ] + +# Run the unit tests +Rake::TestTask.new { |t| + t.libs << "test" + t.pattern = 'test/*_test.rb' + t.verbose = true + t.warning = false +} + + +# Genereate the RDoc documentation +Rake::RDocTask.new { |rdoc| + rdoc.rdoc_dir = 'doc' + rdoc.title = "Action Mailer -- Easy email delivery and testing" + rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' + rdoc.template = "#{ENV['template']}.rb" if ENV['template'] + rdoc.rdoc_files.include('README', 'CHANGELOG') + rdoc.rdoc_files.include('lib/action_mailer.rb') + rdoc.rdoc_files.include('lib/action_mailer/*.rb') +} + + +# Create compressed packages +spec = Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = PKG_NAME + s.summary = "Service layer for easy email delivery and testing." + s.description = %q{Makes it trivial to test and deliver emails sent from a single service layer.} + s.version = PKG_VERSION + + s.author = "David Heinemeier Hansson" + s.email = "david@loudthinking.com" + s.rubyforge_project = "actionmailer" + s.homepage = "http://www.rubyonrails.org" + + s.add_dependency('actionpack', '= 1.12.5' + PKG_BUILD) + + s.has_rdoc = true + s.requirements << 'none' + s.require_path = 'lib' + s.autorequire = 'action_mailer' + + s.files = [ "Rakefile", "install.rb", "README", "CHANGELOG", "MIT-LICENSE" ] + s.files = s.files + Dir.glob( "lib/**/*" ).delete_if { |item| item.include?( "\.svn" ) } + s.files = s.files + Dir.glob( "test/**/*" ).delete_if { |item| item.include?( "\.svn" ) } +end + +Rake::GemPackageTask.new(spec) do |p| + p.gem_spec = spec + p.need_tar = true + p.need_zip = true +end + + +desc "Publish the API documentation" +task :pgem => [:package] do + Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload +end + +desc "Publish the API documentation" +task :pdoc => [:rdoc] do + Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/am", "doc").upload +end + +desc "Publish the release files to RubyForge." +task :release => [ :package ] do + `rubyforge login` + + for ext in %w( gem tgz zip ) + release_command = "rubyforge add_release #{PKG_NAME} #{PKG_NAME} 'REL #{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}" + puts release_command + system(release_command) + end +end \ No newline at end of file diff --git a/vendor/rails/actionmailer/install.rb b/vendor/rails/actionmailer/install.rb new file mode 100644 index 00000000..c559edff --- /dev/null +++ b/vendor/rails/actionmailer/install.rb @@ -0,0 +1,30 @@ +require 'rbconfig' +require 'find' +require 'ftools' + +include Config + +# this was adapted from rdoc's install.rb by way of Log4r + +$sitedir = CONFIG["sitelibdir"] +unless $sitedir + version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"] + $libdir = File.join(CONFIG["libdir"], "ruby", version) + $sitedir = $:.find {|x| x =~ /site_ruby/ } + if !$sitedir + $sitedir = File.join($libdir, "site_ruby") + elsif $sitedir !~ Regexp.quote(version) + $sitedir = File.join($sitedir, version) + end +end + +# the acual gruntwork +Dir.chdir("lib") + +Find.find("action_mailer", "action_mailer.rb") { |f| + if f[-3..-1] == ".rb" + File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true) + else + File::makedirs(File.join($sitedir, *f.split(/\//))) + end +} diff --git a/vendor/rails/actionmailer/lib/action_mailer.rb b/vendor/rails/actionmailer/lib/action_mailer.rb new file mode 100755 index 00000000..a489e32b --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer.rb @@ -0,0 +1,51 @@ +#-- +# Copyright (c) 2004 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +begin + require 'action_controller' +rescue LoadError + begin + require File.dirname(__FILE__) + '/../../actionpack/lib/action_controller' + rescue LoadError + require 'rubygems' + require_gem 'actionpack', '>= 1.9.1' + end +end + +$:.unshift(File.dirname(__FILE__) + "/action_mailer/vendor/") + +require 'action_mailer/base' +require 'action_mailer/helpers' +require 'action_mailer/mail_helper' +require 'action_mailer/quoting' +require 'tmail' +require 'net/smtp' + +ActionMailer::Base.class_eval do + include ActionMailer::Quoting + include ActionMailer::Helpers + + helper MailHelper +end + +silence_warnings { TMail::Encoder.const_set("MAX_LINE_LEN", 200) } \ No newline at end of file diff --git a/vendor/rails/actionmailer/lib/action_mailer/adv_attr_accessor.rb b/vendor/rails/actionmailer/lib/action_mailer/adv_attr_accessor.rb new file mode 100644 index 00000000..50afe4d7 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/adv_attr_accessor.rb @@ -0,0 +1,31 @@ +module ActionMailer + module AdvAttrAccessor #:nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + end + + module ClassMethods #:nodoc: + def adv_attr_accessor(*names) + names.each do |name| + ivar = "@#{name}" + + define_method("#{name}=") do |value| + instance_variable_set(ivar, value) + end + + define_method(name) do |*parameters| + raise ArgumentError, "expected 0 or 1 parameters" unless parameters.length <= 1 + if parameters.empty? + if instance_variables.include?(ivar) + instance_variable_get(ivar) + end + else + instance_variable_set(ivar, parameters.first) + end + end + end + end + end + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/base.rb b/vendor/rails/actionmailer/lib/action_mailer/base.rb new file mode 100644 index 00000000..a67072a9 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/base.rb @@ -0,0 +1,528 @@ +require 'action_mailer/adv_attr_accessor' +require 'action_mailer/part' +require 'action_mailer/part_container' +require 'action_mailer/utils' +require 'tmail/net' + +module ActionMailer #:nodoc: + # ActionMailer allows you to send email from your application using a mailer model and views. + # + # = Mailer Models + # To use ActionMailer, you need to create a mailer model. + # + # $ script/generate mailer Notifier + # + # The generated model inherits from ActionMailer::Base. Emails are defined by creating methods within the model which are then + # used to set variables to be used in the mail template, to change options on the mail, or + # to add attachments. + # + # Examples: + # + # class Notifier < ActionMailer::Base + # def signup_notification(recipient) + # recipients recipient.email_address_with_name + # from "system@example.com" + # subject "New account information" + # body "account" => recipient + # end + # end + # + # Mailer methods have the following configuration methods available. + # + # * recipients - Takes one or more email addresses. These addresses are where your email will be delivered to. Sets the To: header. + # * subject - The subject of your email. Sets the Subject: header. + # * from - Who the email you are sending is from. Sets the From: header. + # * cc - Takes one or more email addresses. These addresses will receive a carbon copy of your email. Sets the Cc: header. + # * bcc - Takes one or more email address. These addresses will receive a blind carbon copy of your email. Sets the Bcc header. + # * sent_on - The date on which the message was sent. If not set, the header wil be set by the delivery agent. + # * content_type - Specify the content type of the message. Defaults to text/plain. + # * headers - Specify additional headers to be set for the message, e.g. headers 'X-Mail-Count' => 107370. + # + # The body method has special behavior. It takes a hash which generates an instance variable + # named after each key in the hash containing the value that that key points to. + # + # So, for example, body "account" => recipient would result + # in an instance variable @account with the value of recipient being accessible in the + # view. + # + # = Mailer Views + # Like ActionController, each mailer class has a corresponding view directory + # in which each method of the class looks for a template with its name. + # To define a template to be used with a mailing, create an .rhtml file with the same name as the method + # in your mailer model. For example, in the mailer defined above, the template at + # app/views/notifier/signup_notification.rhtml would be used to generate the email. + # + # Variables defined in the model are accessible as instance variables in the view. + # + # Emails by default are sent in plain text, so a sample view for our model example might look like this: + # + # Hi <%= @account.name %>, + # Thanks for joining our service! Please check back often. + # + # = Sending Mail + # Once a mailer action and template are defined, you can deliver your message or create it and save it + # for delivery later: + # + # Notifier.deliver_signup_notification(david) # sends the email + # mail = Notifier.create_signup_notification(david) # => a tmail object + # Notifier.deliver(mail) + # + # You never instantiate your mailer class. Rather, your delivery instance + # methods are automatically wrapped in class methods that start with the word + # deliver_ followed by the name of the mailer method that you would + # like to deliver. The signup_notification method defined above is + # delivered by invoking Notifier.deliver_signup_notification. + # + # = HTML Email + # To send mail as HTML, make sure your view (the .rhtml file) generates HTML and + # set the content type to html. + # + # class MyMailer < ActionMailer::Base + # def signup_notification(recipient) + # recipients recipient.email_address_with_name + # subject "New account information" + # body "account" => recipient + # from "system@example.com" + # content_type "text/html" # Here's where the magic happens + # end + # end + # + # = Multipart Email + # You can explicitly specify multipart messages: + # + # class ApplicationMailer < ActionMailer::Base + # def signup_notification(recipient) + # recipients recipient.email_address_with_name + # subject "New account information" + # from "system@example.com" + # + # part :content_type => "text/html", + # :body => render_message("signup-as-html", :account => recipient) + # + # part "text/plain" do |p| + # p.body = render_message("signup-as-plain", :account => recipient) + # p.transfer_encoding = "base64" + # end + # end + # end + # + # Multipart messages can also be used implicitly because ActionMailer will automatically + # detect and use multipart templates, where each template is named after the name of the action, followed + # by the content type. Each such detected template will be added as separate part to the message. + # + # For example, if the following templates existed: + # * signup_notification.text.plain.rhtml + # * signup_notification.text.html.rhtml + # * signup_notification.text.xml.rxml + # * signup_notification.text.x-yaml.rhtml + # + # Each would be rendered and added as a separate part to the message, + # with the corresponding content type. The same body hash is passed to + # each template. + # + # = Attachments + # Attachments can be added by using the +attachment+ method. + # + # Example: + # + # class ApplicationMailer < ActionMailer::Base + # # attachments + # def signup_notification(recipient) + # recipients recipient.email_address_with_name + # subject "New account information" + # from "system@example.com" + # + # attachment :content_type => "image/jpeg", + # :body => File.read("an-image.jpg") + # + # attachment "application/pdf" do |a| + # a.body = generate_your_pdf_here() + # end + # end + # end + # + # = Configuration options + # + # These options are specified on the class level, like ActionMailer::Base.template_root = "/my/templates" + # + # * template_root - template root determines the base from which template references will be made. + # + # * logger - the logger is used for generating information on the mailing run if available. + # Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers. + # + # * server_settings - Allows detailed configuration of the server: + # * :address Allows you to use a remote mail server. Just change it from its default "localhost" setting. + # * :port On the off chance that your mail server doesn't run on port 25, you can change it. + # * :domain If you need to specify a HELO domain, you can do it here. + # * :user_name If your mail server requires authentication, set the username in this setting. + # * :password If your mail server requires authentication, set the password in this setting. + # * :authentication If your mail server requires authentication, you need to specify the authentication type here. + # This is a symbol and one of :plain, :login, :cram_md5 + # + # * raise_delivery_errors - whether or not errors should be raised if the email fails to be delivered. + # + # * delivery_method - Defines a delivery method. Possible values are :smtp (default), :sendmail, and :test. + # Sendmail is assumed to be present at "/usr/sbin/sendmail". + # + # * perform_deliveries - Determines whether deliver_* methods are actually carried out. By default they are, + # but this can be turned off to help functional testing. + # + # * deliveries - Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful + # for unit and functional testing. + # + # * default_charset - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also + # pick a different charset from inside a method with @charset. + # * default_content_type - The default content type used for the main part of the message. Defaults to "text/plain". You + # can also pick a different content type from inside a method with @content_type. + # * default_mime_version - The default mime version used for the message. Defaults to nil. You + # can also pick a different value from inside a method with @mime_version. When multipart messages are in + # use, @mime_version will be set to "1.0" if it is not set inside a method. + # * default_implicit_parts_order - When a message is built implicitly (i.e. multiple parts are assembled from templates + # which specify the content type in their filenames) this variable controls how the parts are ordered. Defaults to + # ["text/html", "text/enriched", "text/plain"]. Items that appear first in the array have higher priority in the mail client + # and appear last in the mime encoded message. You can also pick a different order from inside a method with + # @implicit_parts_order. + class Base + include AdvAttrAccessor, PartContainer + + # Action Mailer subclasses should be reloaded by the dispatcher in Rails + # when Dependencies.mechanism = :load. + include Reloadable::Subclasses + + private_class_method :new #:nodoc: + + class_inheritable_accessor :template_root + cattr_accessor :logger + + @@server_settings = { + :address => "localhost", + :port => 25, + :domain => 'localhost.localdomain', + :user_name => nil, + :password => nil, + :authentication => nil + } + cattr_accessor :server_settings + + @@raise_delivery_errors = true + cattr_accessor :raise_delivery_errors + + @@delivery_method = :smtp + cattr_accessor :delivery_method + + @@perform_deliveries = true + cattr_accessor :perform_deliveries + + @@deliveries = [] + cattr_accessor :deliveries + + @@default_charset = "utf-8" + cattr_accessor :default_charset + + @@default_content_type = "text/plain" + cattr_accessor :default_content_type + + @@default_mime_version = nil + cattr_accessor :default_mime_version + + @@default_implicit_parts_order = [ "text/html", "text/enriched", "text/plain" ] + cattr_accessor :default_implicit_parts_order + + # Specify the BCC addresses for the message + adv_attr_accessor :bcc + + # Define the body of the message. This is either a Hash (in which case it + # specifies the variables to pass to the template when it is rendered), + # or a string, in which case it specifies the actual text of the message. + adv_attr_accessor :body + + # Specify the CC addresses for the message. + adv_attr_accessor :cc + + # Specify the charset to use for the message. This defaults to the + # +default_charset+ specified for ActionMailer::Base. + adv_attr_accessor :charset + + # Specify the content type for the message. This defaults to text/plain + # in most cases, but can be automatically set in some situations. + adv_attr_accessor :content_type + + # Specify the from address for the message. + adv_attr_accessor :from + + # Specify additional headers to be added to the message. + adv_attr_accessor :headers + + # Specify the order in which parts should be sorted, based on content-type. + # This defaults to the value for the +default_implicit_parts_order+. + adv_attr_accessor :implicit_parts_order + + # Override the mailer name, which defaults to an inflected version of the + # mailer's class name. If you want to use a template in a non-standard + # location, you can use this to specify that location. + adv_attr_accessor :mailer_name + + # Defaults to "1.0", but may be explicitly given if needed. + adv_attr_accessor :mime_version + + # The recipient addresses for the message, either as a string (for a single + # address) or an array (for multiple addresses). + adv_attr_accessor :recipients + + # The date on which the message was sent. If not set (the default), the + # header will be set by the delivery agent. + adv_attr_accessor :sent_on + + # Specify the subject of the message. + adv_attr_accessor :subject + + # Specify the template name to use for current message. This is the "base" + # template name, without the extension or directory, and may be used to + # have multiple mailer methods share the same template. + adv_attr_accessor :template + + # The mail object instance referenced by this mailer. + attr_reader :mail + + class << self + def method_missing(method_symbol, *parameters)#:nodoc: + case method_symbol.id2name + when /^create_([_a-z]\w*)/ then new($1, *parameters).mail + when /^deliver_([_a-z]\w*)/ then new($1, *parameters).deliver! + when "new" then nil + else super + end + end + + # Receives a raw email, parses it into an email object, decodes it, + # instantiates a new mailer, and passes the email object to the mailer + # object's #receive method. If you want your mailer to be able to + # process incoming messages, you'll need to implement a #receive + # method that accepts the email object as a parameter: + # + # class MyMailer < ActionMailer::Base + # def receive(mail) + # ... + # end + # end + def receive(raw_email) + logger.info "Received mail:\n #{raw_email}" unless logger.nil? + mail = TMail::Mail.parse(raw_email) + mail.base64_decode + new.receive(mail) + end + + # Deliver the given mail object directly. This can be used to deliver + # a preconstructed mail object, like: + # + # email = MyMailer.create_some_mail(parameters) + # email.set_some_obscure_header "frobnicate" + # MyMailer.deliver(email) + def deliver(mail) + new.deliver!(mail) + end + end + + # Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer + # will be initialized according to the named method. If not, the mailer will + # remain uninitialized (useful when you only need to invoke the "receive" + # method, for instance). + def initialize(method_name=nil, *parameters) #:nodoc: + create!(method_name, *parameters) if method_name + end + + # Initialize the mailer via the given +method_name+. The body will be + # rendered and a new TMail::Mail object created. + def create!(method_name, *parameters) #:nodoc: + initialize_defaults(method_name) + send(method_name, *parameters) + + # If an explicit, textual body has not been set, we check assumptions. + unless String === @body + # First, we look to see if there are any likely templates that match, + # which include the content-type in their file name (i.e., + # "the_template_file.text.html.rhtml", etc.). Only do this if parts + # have not already been specified manually. + if @parts.empty? + templates = Dir.glob("#{template_path}/#{@template}.*") + templates.each do |path| + # TODO: don't hardcode rhtml|rxml + basename = File.basename(path) + next unless md = /^([^\.]+)\.([^\.]+\.[^\+]+)\.(rhtml|rxml)$/.match(basename) + template_name = basename + content_type = md.captures[1].gsub('.', '/') + @parts << Part.new(:content_type => content_type, + :disposition => "inline", :charset => charset, + :body => render_message(template_name, @body)) + end + unless @parts.empty? + @content_type = "multipart/alternative" + @parts = sort_parts(@parts, @implicit_parts_order) + end + end + + # Then, if there were such templates, we check to see if we ought to + # also render a "normal" template (without the content type). If a + # normal template exists (or if there were no implicit parts) we render + # it. + template_exists = @parts.empty? + template_exists ||= Dir.glob("#{template_path}/#{@template}.*").any? { |i| File.basename(i).split(".").length == 2 } + @body = render_message(@template, @body) if template_exists + + # Finally, if there are other message parts and a textual body exists, + # we shift it onto the front of the parts and set the body to nil (so + # that create_mail doesn't try to render it in addition to the parts). + if !@parts.empty? && String === @body + @parts.unshift Part.new(:charset => charset, :body => @body) + @body = nil + end + end + + # If this is a multipart e-mail add the mime_version if it is not + # already set. + @mime_version ||= "1.0" if !@parts.empty? + + # build the mail object itself + @mail = create_mail + end + + # Delivers a TMail::Mail object. By default, it delivers the cached mail + # object (from the #create! method). If no cached mail object exists, and + # no alternate has been given as the parameter, this will fail. + def deliver!(mail = @mail) + raise "no mail object available for delivery!" unless mail + logger.info "Sent mail:\n #{mail.encoded}" unless logger.nil? + + begin + send("perform_delivery_#{delivery_method}", mail) if perform_deliveries + rescue Object => e + raise e if raise_delivery_errors + end + + return mail + end + + private + # Set up the default values for the various instance variables of this + # mailer. Subclasses may override this method to provide different + # defaults. + def initialize_defaults(method_name) + @charset ||= @@default_charset.dup + @content_type ||= @@default_content_type.dup + @implicit_parts_order ||= @@default_implicit_parts_order.dup + @template ||= method_name + @mailer_name ||= Inflector.underscore(self.class.name) + @parts ||= [] + @headers ||= {} + @body ||= {} + @mime_version = @@default_mime_version.dup if @@default_mime_version + end + + def render_message(method_name, body) + render :file => method_name, :body => body + end + + def render(opts) + body = opts.delete(:body) + initialize_template_class(body).render(opts) + end + + def template_path + "#{template_root}/#{mailer_name}" + end + + def initialize_template_class(assigns) + ActionView::Base.new(template_path, assigns, self) + end + + def sort_parts(parts, order = []) + order = order.collect { |s| s.downcase } + + parts = parts.sort do |a, b| + a_ct = a.content_type.downcase + b_ct = b.content_type.downcase + + a_in = order.include? a_ct + b_in = order.include? b_ct + + s = case + when a_in && b_in + order.index(a_ct) <=> order.index(b_ct) + when a_in + -1 + when b_in + 1 + else + a_ct <=> b_ct + end + + # reverse the ordering because parts that come last are displayed + # first in mail clients + (s * -1) + end + + parts + end + + def create_mail + m = TMail::Mail.new + + m.subject, = quote_any_if_necessary(charset, subject) + m.to, m.from = quote_any_address_if_necessary(charset, recipients, from) + m.bcc = quote_address_if_necessary(bcc, charset) unless bcc.nil? + m.cc = quote_address_if_necessary(cc, charset) unless cc.nil? + + m.mime_version = mime_version unless mime_version.nil? + m.date = sent_on.to_time rescue sent_on if sent_on + headers.each { |k, v| m[k] = v } + + real_content_type, ctype_attrs = parse_content_type + + if @parts.empty? + m.set_content_type(real_content_type, nil, ctype_attrs) + m.body = Utils.normalize_new_lines(body) + else + if String === body + part = TMail::Mail.new + part.body = Utils.normalize_new_lines(body) + part.set_content_type(real_content_type, nil, ctype_attrs) + part.set_content_disposition "inline" + m.parts << part + end + + @parts.each do |p| + part = (TMail::Mail === p ? p : p.to_mail(self)) + m.parts << part + end + + if real_content_type =~ /multipart/ + ctype_attrs.delete "charset" + m.set_content_type(real_content_type, nil, ctype_attrs) + end + end + + @mail = m + end + + def perform_delivery_smtp(mail) + destinations = mail.destinations + mail.ready_to_send + + Net::SMTP.start(server_settings[:address], server_settings[:port], server_settings[:domain], + server_settings[:user_name], server_settings[:password], server_settings[:authentication]) do |smtp| + smtp.sendmail(mail.encoded, mail.from, destinations) + end + end + + def perform_delivery_sendmail(mail) + IO.popen("/usr/sbin/sendmail -i -t","w+") do |sm| + sm.print(mail.encoded.gsub(/\r/, '')) + sm.flush + end + end + + def perform_delivery_test(mail) + deliveries << mail + end + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/helpers.rb b/vendor/rails/actionmailer/lib/action_mailer/helpers.rb new file mode 100644 index 00000000..b53326ca --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/helpers.rb @@ -0,0 +1,115 @@ +module ActionMailer + module Helpers #:nodoc: + def self.append_features(base) #:nodoc: + super + + # Initialize the base module to aggregate its helpers. + base.class_inheritable_accessor :master_helper_module + base.master_helper_module = Module.new + + # Extend base with class methods to declare helpers. + base.extend(ClassMethods) + + base.class_eval do + # Wrap inherited to create a new master helper module for subclasses. + class << self + alias_method :inherited_without_helper, :inherited + alias_method :inherited, :inherited_with_helper + end + + # Wrap initialize_template_class to extend new template class + # instances with the master helper module. + alias_method :initialize_template_class_without_helper, :initialize_template_class + alias_method :initialize_template_class, :initialize_template_class_with_helper + end + end + + module ClassMethods + # Makes all the (instance) methods in the helper module available to templates rendered through this controller. + # See ActionView::Helpers (link:classes/ActionView/Helpers.html) for more about making your own helper modules + # available to the templates. + def add_template_helper(helper_module) #:nodoc: + master_helper_module.module_eval "include #{helper_module}" + end + + # Declare a helper: + # helper :foo + # requires 'foo_helper' and includes FooHelper in the template class. + # helper FooHelper + # includes FooHelper in the template class. + # helper { def foo() "#{bar} is the very best" end } + # evaluates the block in the template class, adding method #foo. + # helper(:three, BlindHelper) { def mice() 'mice' end } + # does all three. + def helper(*args, &block) + args.flatten.each do |arg| + case arg + when Module + add_template_helper(arg) + when String, Symbol + file_name = arg.to_s.underscore + '_helper' + class_name = file_name.camelize + + begin + require_dependency(file_name) + rescue LoadError => load_error + requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1] + msg = (requiree == file_name) ? "Missing helper file helpers/#{file_name}.rb" : "Can't load file: #{requiree}" + raise LoadError.new(msg).copy_blame!(load_error) + end + + add_template_helper(class_name.constantize) + else + raise ArgumentError, 'helper expects String, Symbol, or Module argument' + end + end + + # Evaluate block in template class if given. + master_helper_module.module_eval(&block) if block_given? + end + + # Declare a controller method as a helper. For example, + # helper_method :link_to + # def link_to(name, options) ... end + # makes the link_to controller method available in the view. + def helper_method(*methods) + methods.flatten.each do |method| + master_helper_module.module_eval <<-end_eval + def #{method}(*args, &block) + controller.send(%(#{method}), *args, &block) + end + end_eval + end + end + + # Declare a controller attribute as a helper. For example, + # helper_attr :name + # attr_accessor :name + # makes the name and name= controller methods available in the view. + # The is a convenience wrapper for helper_method. + def helper_attr(*attrs) + attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") } + end + + private + def inherited_with_helper(child) + inherited_without_helper(child) + begin + child.master_helper_module = Module.new + child.master_helper_module.send :include, master_helper_module + child.helper child.name.underscore + rescue MissingSourceFile => e + raise unless e.is_missing?("helpers/#{child.name.underscore}_helper") + end + end + end + + private + # Extend the template class instance with our controller's helper module. + def initialize_template_class_with_helper(assigns) + returning(template = initialize_template_class_without_helper(assigns)) do + template.extend self.class.master_helper_module + end + end + end +end \ No newline at end of file diff --git a/vendor/rails/actionmailer/lib/action_mailer/mail_helper.rb b/vendor/rails/actionmailer/lib/action_mailer/mail_helper.rb new file mode 100644 index 00000000..11fd7d77 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/mail_helper.rb @@ -0,0 +1,19 @@ +require 'text/format' + +module MailHelper + # Uses Text::Format to take the text and format it, indented two spaces for + # each line, and wrapped at 72 columns. + def block_format(text) + formatted = text.split(/\n\r\n/).collect { |paragraph| + Text::Format.new( + :columns => 72, :first_indent => 2, :body_indent => 2, :text => paragraph + ).format + }.join("\n") + + # Make list points stand on their own line + formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { |s| " #{$1} #{$2.strip}\n" } + formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { |s| " #{$1} #{$2.strip}\n" } + + formatted + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/part.rb b/vendor/rails/actionmailer/lib/action_mailer/part.rb new file mode 100644 index 00000000..31f5b441 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/part.rb @@ -0,0 +1,113 @@ +require 'action_mailer/adv_attr_accessor' +require 'action_mailer/part_container' +require 'action_mailer/utils' + +module ActionMailer + # Represents a subpart of an email message. It shares many similar + # attributes of ActionMailer::Base. Although you can create parts manually + # and add them to the #parts list of the mailer, it is easier + # to use the helper methods in ActionMailer::PartContainer. + class Part + include ActionMailer::AdvAttrAccessor + include ActionMailer::PartContainer + + # Represents the body of the part, as a string. This should not be a + # Hash (like ActionMailer::Base), but if you want a template to be rendered + # into the body of a subpart you can do it with the mailer's #render method + # and assign the result here. + adv_attr_accessor :body + + # Specify the charset for this subpart. By default, it will be the charset + # of the containing part or mailer. + adv_attr_accessor :charset + + # The content disposition of this part, typically either "inline" or + # "attachment". + adv_attr_accessor :content_disposition + + # The content type of the part. + adv_attr_accessor :content_type + + # The filename to use for this subpart (usually for attachments). + adv_attr_accessor :filename + + # Accessor for specifying additional headers to include with this part. + adv_attr_accessor :headers + + # The transfer encoding to use for this subpart, like "base64" or + # "quoted-printable". + adv_attr_accessor :transfer_encoding + + # Create a new part from the given +params+ hash. The valid params keys + # correspond to the accessors. + def initialize(params) + @content_type = params[:content_type] + @content_disposition = params[:disposition] || "inline" + @charset = params[:charset] + @body = params[:body] + @filename = params[:filename] + @transfer_encoding = params[:transfer_encoding] || "quoted-printable" + @headers = params[:headers] || {} + @parts = [] + end + + # Convert the part to a mail object which can be included in the parts + # list of another mail object. + def to_mail(defaults) + part = TMail::Mail.new + + real_content_type, ctype_attrs = parse_content_type(defaults) + + if @parts.empty? + part.content_transfer_encoding = transfer_encoding || "quoted-printable" + case (transfer_encoding || "").downcase + when "base64" then + part.body = TMail::Base64.folding_encode(body) + when "quoted-printable" + part.body = [Utils.normalize_new_lines(body)].pack("M*") + else + part.body = body + end + + # Always set the content_type after setting the body and or parts! + # Also don't set filename and name when there is none (like in + # non-attachment parts) + if content_disposition == "attachment" + ctype_attrs.delete "charset" + part.set_content_type(real_content_type, nil, + squish("name" => filename).merge(ctype_attrs)) + part.set_content_disposition(content_disposition, + squish("filename" => filename).merge(ctype_attrs)) + else + part.set_content_type(real_content_type, nil, ctype_attrs) + part.set_content_disposition(content_disposition) + end + else + if String === body + part = TMail::Mail.new + part.body = body + part.set_content_type(real_content_type, nil, ctype_attrs) + part.set_content_disposition "inline" + m.parts << part + end + + @parts.each do |p| + prt = (TMail::Mail === p ? p : p.to_mail(defaults)) + part.parts << prt + end + + part.set_content_type(real_content_type, nil, ctype_attrs) if real_content_type =~ /multipart/ + end + + headers.each { |k,v| part[k] = v } + + part + end + + private + + def squish(values={}) + values.delete_if { |k,v| v.nil? } + end + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/part_container.rb b/vendor/rails/actionmailer/lib/action_mailer/part_container.rb new file mode 100644 index 00000000..3e3d6b9d --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/part_container.rb @@ -0,0 +1,51 @@ +module ActionMailer + # Accessors and helpers that ActionMailer::Base and ActionMailer::Part have + # in common. Using these helpers you can easily add subparts or attachments + # to your message: + # + # def my_mail_message(...) + # ... + # part "text/plain" do |p| + # p.body "hello, world" + # p.transfer_encoding "base64" + # end + # + # attachment "image/jpg" do |a| + # a.body = File.read("hello.jpg") + # a.filename = "hello.jpg" + # end + # end + module PartContainer + # The list of subparts of this container + attr_reader :parts + + # Add a part to a multipart message, with the given content-type. The + # part itself is yielded to the block so that other properties (charset, + # body, headers, etc.) can be set on it. + def part(params) + params = {:content_type => params} if String === params + part = Part.new(params) + yield part if block_given? + @parts << part + end + + # Add an attachment to a multipart message. This is simply a part with the + # content-disposition set to "attachment". + def attachment(params, &block) + params = { :content_type => params } if String === params + params = { :disposition => "attachment", + :transfer_encoding => "base64" }.merge(params) + part(params, &block) + end + + private + + def parse_content_type(defaults=nil) + return [defaults && defaults.content_type, {}] if content_type.blank? + ctype, *attrs = content_type.split(/;\s*/) + attrs = attrs.inject({}) { |h,s| k,v = s.split(/=/, 2); h[k] = v; h } + [ctype, {"charset" => charset || defaults && defaults.charset}.merge(attrs)] + end + + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/quoting.rb b/vendor/rails/actionmailer/lib/action_mailer/quoting.rb new file mode 100644 index 00000000..d6e04e4d --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/quoting.rb @@ -0,0 +1,59 @@ +module ActionMailer + module Quoting #:nodoc: + # Convert the given text into quoted printable format, with an instruction + # that the text be eventually interpreted in the given charset. + def quoted_printable(text, charset) + text = text.gsub( /[^a-z ]/i ) { quoted_printable_encode($&) }. + gsub( / /, "_" ) + "=?#{charset}?Q?#{text}?=" + end + + # Convert the given character to quoted printable format, taking into + # account multi-byte characters (if executing with $KCODE="u", for instance) + def quoted_printable_encode(character) + result = "" + character.each_byte { |b| result << "=%02x" % b } + result + end + + # A quick-and-dirty regexp for determining whether a string contains any + # characters that need escaping. + if !defined?(CHARS_NEEDING_QUOTING) + CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/ + end + + # Quote the given text if it contains any "illegal" characters + def quote_if_necessary(text, charset) + (text =~ CHARS_NEEDING_QUOTING) ? + quoted_printable(text, charset) : + text + end + + # Quote any of the given strings if they contain any "illegal" characters + def quote_any_if_necessary(charset, *args) + args.map { |v| quote_if_necessary(v, charset) } + end + + # Quote the given address if it needs to be. The address may be a + # regular email address, or it can be a phrase followed by an address in + # brackets. The phrase is the only part that will be quoted, and only if + # it needs to be. This allows extended characters to be used in the + # "to", "from", "cc", and "bcc" headers. + def quote_address_if_necessary(address, charset) + if Array === address + address.map { |a| quote_address_if_necessary(a, charset) } + elsif address =~ /^(\S.*)\s+(<.*>)$/ + address = $2 + phrase = quote_if_necessary($1.gsub(/^['"](.*)['"]$/, '\1'), charset) + "\"#{phrase}\" #{address}" + else + address + end + end + + # Quote any of the given addresses, if they need to be. + def quote_any_address_if_necessary(charset, *args) + args.map { |v| quote_address_if_necessary(v, charset) } + end + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/utils.rb b/vendor/rails/actionmailer/lib/action_mailer/utils.rb new file mode 100644 index 00000000..552f695a --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/utils.rb @@ -0,0 +1,8 @@ +module ActionMailer + module Utils #:nodoc: + def normalize_new_lines(text) + text.to_s.gsub(/\r\n?/, "\n") + end + module_function :normalize_new_lines + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/text/format.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/text/format.rb new file mode 100755 index 00000000..de054db8 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/text/format.rb @@ -0,0 +1,1466 @@ +#-- +# Text::Format for Ruby +# Version 0.63 +# +# Copyright (c) 2002 - 2003 Austin Ziegler +# +# $Id: format.rb,v 1.1.1.1 2004/10/14 11:59:57 webster132 Exp $ +# +# ========================================================================== +# Revision History :: +# YYYY.MM.DD Change ID Developer +# Description +# -------------------------------------------------------------------------- +# 2002.10.18 Austin Ziegler +# Fixed a minor problem with tabs not being counted. Changed +# abbreviations from Hash to Array to better suit Ruby's +# capabilities. Fixed problems with the way that Array arguments +# are handled in calls to the major object types, excepting in +# Text::Format#expand and Text::Format#unexpand (these will +# probably need to be fixed). +# 2002.10.30 Austin Ziegler +# Fixed the ordering of the <=> for binary tests. Fixed +# Text::Format#expand and Text::Format#unexpand to handle array +# arguments better. +# 2003.01.24 Austin Ziegler +# Fixed a problem with Text::Format::RIGHT_FILL handling where a +# single word is larger than #columns. Removed Comparable +# capabilities (<=> doesn't make sense; == does). Added Symbol +# equivalents for the Hash initialization. Hash initialization has +# been modified so that values are set as follows (Symbols are +# highest priority; strings are middle; defaults are lowest): +# @columns = arg[:columns] || arg['columns'] || @columns +# Added #hard_margins, #split_rules, #hyphenator, and #split_words. +# 2003.02.07 Austin Ziegler +# Fixed the installer for proper case-sensitive handling. +# 2003.03.28 Austin Ziegler +# Added the ability for a hyphenator to receive the formatter +# object. Fixed a bug for strings matching /\A\s*\Z/ failing +# entirely. Fixed a test case failing under 1.6.8. +# 2003.04.04 Austin Ziegler +# Handle the case of hyphenators returning nil for first/rest. +# 2003.09.17 Austin Ziegler +# Fixed a problem where #paragraphs(" ") was raising +# NoMethodError. +# +# ========================================================================== +#++ + +module Text #:nodoc: + # Text::Format for Ruby is copyright 2002 - 2005 by Austin Ziegler. It + # is available under Ruby's licence, the Perl Artistic licence, or the + # GNU GPL version 2 (or at your option, any later version). As a + # special exception, for use with official Rails (provided by the + # rubyonrails.org development team) and any project created with + # official Rails, the following alternative MIT-style licence may be + # used: + # + # == Text::Format Licence for Rails and Rails Applications + # Permission is hereby granted, free of charge, to any person + # obtaining a copy of this software and associated documentation files + # (the "Software"), to deal in the Software without restriction, + # including without limitation the rights to use, copy, modify, merge, + # publish, distribute, sublicense, and/or sell copies of the Software, + # and to permit persons to whom the Software is furnished to do so, + # subject to the following conditions: + # + # * The names of its contributors may not be used to endorse or + # promote products derived from this software without specific prior + # written permission. + # + # The above copyright notice and this permission notice shall be + # included in all copies or substantial portions of the Software. + # + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + # SOFTWARE. + class Format + VERSION = '0.63' + + # Local abbreviations. More can be added with Text::Format.abbreviations + ABBREV = [ 'Mr', 'Mrs', 'Ms', 'Jr', 'Sr' ] + + # Formatting values + LEFT_ALIGN = 0 + RIGHT_ALIGN = 1 + RIGHT_FILL = 2 + JUSTIFY = 3 + + # Word split modes (only applies when #hard_margins is true). + SPLIT_FIXED = 1 + SPLIT_CONTINUATION = 2 + SPLIT_HYPHENATION = 4 + SPLIT_CONTINUATION_FIXED = SPLIT_CONTINUATION | SPLIT_FIXED + SPLIT_HYPHENATION_FIXED = SPLIT_HYPHENATION | SPLIT_FIXED + SPLIT_HYPHENATION_CONTINUATION = SPLIT_HYPHENATION | SPLIT_CONTINUATION + SPLIT_ALL = SPLIT_HYPHENATION | SPLIT_CONTINUATION | SPLIT_FIXED + + # Words forcibly split by Text::Format will be stored as split words. + # This class represents a word forcibly split. + class SplitWord + # The word that was split. + attr_reader :word + # The first part of the word that was split. + attr_reader :first + # The remainder of the word that was split. + attr_reader :rest + + def initialize(word, first, rest) #:nodoc: + @word = word + @first = first + @rest = rest + end + end + + private + LEQ_RE = /[.?!]['"]?$/ + + def brk_re(i) #:nodoc: + %r/((?:\S+\s+){#{i}})(.+)/ + end + + def posint(p) #:nodoc: + p.to_i.abs + end + + public + # Compares two Text::Format objects. All settings of the objects are + # compared *except* #hyphenator. Generated results (e.g., #split_words) + # are not compared, either. + def ==(o) + (@text == o.text) && + (@columns == o.columns) && + (@left_margin == o.left_margin) && + (@right_margin == o.right_margin) && + (@hard_margins == o.hard_margins) && + (@split_rules == o.split_rules) && + (@first_indent == o.first_indent) && + (@body_indent == o.body_indent) && + (@tag_text == o.tag_text) && + (@tabstop == o.tabstop) && + (@format_style == o.format_style) && + (@extra_space == o.extra_space) && + (@tag_paragraph == o.tag_paragraph) && + (@nobreak == o.nobreak) && + (@abbreviations == o.abbreviations) && + (@nobreak_regex == o.nobreak_regex) + end + + # The text to be manipulated. Note that value is optional, but if the + # formatting functions are called without values, this text is what will + # be formatted. + # + # *Default*:: [] + # Used in:: All methods + attr_accessor :text + + # The total width of the format area. The margins, indentation, and text + # are formatted into this space. + # + # COLUMNS + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin indent text is formatted into here right margin + # + # *Default*:: 72 + # Used in:: #format, #paragraphs, + # #center + attr_reader :columns + + # The total width of the format area. The margins, indentation, and text + # are formatted into this space. The value provided is silently + # converted to a positive integer. + # + # COLUMNS + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin indent text is formatted into here right margin + # + # *Default*:: 72 + # Used in:: #format, #paragraphs, + # #center + def columns=(c) + @columns = posint(c) + end + + # The number of spaces used for the left margin. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # LEFT MARGIN indent text is formatted into here right margin + # + # *Default*:: 0 + # Used in:: #format, #paragraphs, + # #center + attr_reader :left_margin + + # The number of spaces used for the left margin. The value provided is + # silently converted to a positive integer value. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # LEFT MARGIN indent text is formatted into here right margin + # + # *Default*:: 0 + # Used in:: #format, #paragraphs, + # #center + def left_margin=(left) + @left_margin = posint(left) + end + + # The number of spaces used for the right margin. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin indent text is formatted into here RIGHT MARGIN + # + # *Default*:: 0 + # Used in:: #format, #paragraphs, + # #center + attr_reader :right_margin + + # The number of spaces used for the right margin. The value provided is + # silently converted to a positive integer value. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin indent text is formatted into here RIGHT MARGIN + # + # *Default*:: 0 + # Used in:: #format, #paragraphs, + # #center + def right_margin=(r) + @right_margin = posint(r) + end + + # The number of spaces to indent the first line of a paragraph. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin INDENT text is formatted into here right margin + # + # *Default*:: 4 + # Used in:: #format, #paragraphs + attr_reader :first_indent + + # The number of spaces to indent the first line of a paragraph. The + # value provided is silently converted to a positive integer value. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin INDENT text is formatted into here right margin + # + # *Default*:: 4 + # Used in:: #format, #paragraphs + def first_indent=(f) + @first_indent = posint(f) + end + + # The number of spaces to indent all lines after the first line of a + # paragraph. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin INDENT text is formatted into here right margin + # + # *Default*:: 0 + # Used in:: #format, #paragraphs + attr_reader :body_indent + + # The number of spaces to indent all lines after the first line of + # a paragraph. The value provided is silently converted to a + # positive integer value. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin INDENT text is formatted into here right margin + # + # *Default*:: 0 + # Used in:: #format, #paragraphs + def body_indent=(b) + @body_indent = posint(b) + end + + # Normally, words larger than the format area will be placed on a line + # by themselves. Setting this to +true+ will force words larger than the + # format area to be split into one or more "words" each at most the size + # of the format area. The first line and the original word will be + # placed into #split_words. Note that this will cause the + # output to look *similar* to a #format_style of JUSTIFY. (Lines will be + # filled as much as possible.) + # + # *Default*:: +false+ + # Used in:: #format, #paragraphs + attr_accessor :hard_margins + + # An array of words split during formatting if #hard_margins is set to + # +true+. + # #split_words << Text::Format::SplitWord.new(word, first, rest) + attr_reader :split_words + + # The object responsible for hyphenating. It must respond to + # #hyphenate_to(word, size) or #hyphenate_to(word, size, formatter) and + # return an array of the word split into two parts; if there is a + # hyphenation mark to be applied, responsibility belongs to the + # hyphenator object. The size is the MAXIMUM size permitted, including + # any hyphenation marks. If the #hyphenate_to method has an arity of 3, + # the formatter will be provided to the method. This allows the + # hyphenator to make decisions about the hyphenation based on the + # formatting rules. + # + # *Default*:: +nil+ + # Used in:: #format, #paragraphs + attr_reader :hyphenator + + # The object responsible for hyphenating. It must respond to + # #hyphenate_to(word, size) and return an array of the word hyphenated + # into two parts. The size is the MAXIMUM size permitted, including any + # hyphenation marks. + # + # *Default*:: +nil+ + # Used in:: #format, #paragraphs + def hyphenator=(h) + raise ArgumentError, "#{h.inspect} is not a valid hyphenator." unless h.respond_to?(:hyphenate_to) + arity = h.method(:hyphenate_to).arity + raise ArgumentError, "#{h.inspect} must have exactly two or three arguments." unless [2, 3].include?(arity) + @hyphenator = h + @hyphenator_arity = arity + end + + # Specifies the split mode; used only when #hard_margins is set to + # +true+. Allowable values are: + # [+SPLIT_FIXED+] The word will be split at the number of + # characters needed, with no marking at all. + # repre + # senta + # ion + # [+SPLIT_CONTINUATION+] The word will be split at the number of + # characters needed, with a C-style continuation + # character. If a word is the only item on a + # line and it cannot be split into an + # appropriate size, SPLIT_FIXED will be used. + # repr\ + # esen\ + # tati\ + # on + # [+SPLIT_HYPHENATION+] The word will be split according to the + # hyphenator specified in #hyphenator. If there + # is no #hyphenator specified, works like + # SPLIT_CONTINUATION. The example is using + # TeX::Hyphen. If a word is the only item on a + # line and it cannot be split into an + # appropriate size, SPLIT_CONTINUATION mode will + # be used. + # rep- + # re- + # sen- + # ta- + # tion + # + # *Default*:: Text::Format::SPLIT_FIXED + # Used in:: #format, #paragraphs + attr_reader :split_rules + + # Specifies the split mode; used only when #hard_margins is set to + # +true+. Allowable values are: + # [+SPLIT_FIXED+] The word will be split at the number of + # characters needed, with no marking at all. + # repre + # senta + # ion + # [+SPLIT_CONTINUATION+] The word will be split at the number of + # characters needed, with a C-style continuation + # character. + # repr\ + # esen\ + # tati\ + # on + # [+SPLIT_HYPHENATION+] The word will be split according to the + # hyphenator specified in #hyphenator. If there + # is no #hyphenator specified, works like + # SPLIT_CONTINUATION. The example is using + # TeX::Hyphen as the #hyphenator. + # rep- + # re- + # sen- + # ta- + # tion + # + # These values can be bitwise ORed together (e.g., SPLIT_FIXED | + # SPLIT_CONTINUATION) to provide fallback split methods. In the + # example given, an attempt will be made to split the word using the + # rules of SPLIT_CONTINUATION; if there is not enough room, the word + # will be split with the rules of SPLIT_FIXED. These combinations are + # also available as the following values: + # * +SPLIT_CONTINUATION_FIXED+ + # * +SPLIT_HYPHENATION_FIXED+ + # * +SPLIT_HYPHENATION_CONTINUATION+ + # * +SPLIT_ALL+ + # + # *Default*:: Text::Format::SPLIT_FIXED + # Used in:: #format, #paragraphs + def split_rules=(s) + raise ArgumentError, "Invalid value provided for split_rules." if ((s < SPLIT_FIXED) || (s > SPLIT_ALL)) + @split_rules = s + end + + # Indicates whether sentence terminators should be followed by a single + # space (+false+), or two spaces (+true+). + # + # *Default*:: +false+ + # Used in:: #format, #paragraphs + attr_accessor :extra_space + + # Defines the current abbreviations as an array. This is only used if + # extra_space is turned on. + # + # If one is abbreviating "President" as "Pres." (abbreviations = + # ["Pres"]), then the results of formatting will be as illustrated in + # the table below: + # + # extra_space | include? | !include? + # true | Pres. Lincoln | Pres. Lincoln + # false | Pres. Lincoln | Pres. Lincoln + # + # *Default*:: {} + # Used in:: #format, #paragraphs + attr_accessor :abbreviations + + # Indicates whether the formatting of paragraphs should be done with + # tagged paragraphs. Useful only with #tag_text. + # + # *Default*:: +false+ + # Used in:: #format, #paragraphs + attr_accessor :tag_paragraph + + # The array of text to be placed before each paragraph when + # #tag_paragraph is +true+. When #format() is called, + # only the first element of the array is used. When #paragraphs + # is called, then each entry in the array will be used once, with + # corresponding paragraphs. If the tag elements are exhausted before the + # text is exhausted, then the remaining paragraphs will not be tagged. + # Regardless of indentation settings, a blank line will be inserted + # between all paragraphs when #tag_paragraph is +true+. + # + # *Default*:: [] + # Used in:: #format, #paragraphs + attr_accessor :tag_text + + # Indicates whether or not the non-breaking space feature should be + # used. + # + # *Default*:: +false+ + # Used in:: #format, #paragraphs + attr_accessor :nobreak + + # A hash which holds the regular expressions on which spaces should not + # be broken. The hash is set up such that the key is the first word and + # the value is the second word. + # + # For example, if +nobreak_regex+ contains the following hash: + # + # { '^Mrs?\.$' => '\S+$', '^\S+$' => '^(?:S|J)r\.$'} + # + # Then "Mr. Jones", "Mrs. Jones", and "Jones Jr." would not be broken. + # If this simple matching algorithm indicates that there should not be a + # break at the current end of line, then a backtrack is done until there + # are two words on which line breaking is permitted. If two such words + # are not found, then the end of the line will be broken *regardless*. + # If there is a single word on the current line, then no backtrack is + # done and the word is stuck on the end. + # + # *Default*:: {} + # Used in:: #format, #paragraphs + attr_accessor :nobreak_regex + + # Indicates the number of spaces that a single tab represents. + # + # *Default*:: 8 + # Used in:: #expand, #unexpand, + # #paragraphs + attr_reader :tabstop + + # Indicates the number of spaces that a single tab represents. + # + # *Default*:: 8 + # Used in:: #expand, #unexpand, + # #paragraphs + def tabstop=(t) + @tabstop = posint(t) + end + + # Specifies the format style. Allowable values are: + # [+LEFT_ALIGN+] Left justified, ragged right. + # |A paragraph that is| + # |left aligned.| + # [+RIGHT_ALIGN+] Right justified, ragged left. + # |A paragraph that is| + # | right aligned.| + # [+RIGHT_FILL+] Left justified, right ragged, filled to width by + # spaces. (Essentially the same as +LEFT_ALIGN+ except + # that lines are padded on the right.) + # |A paragraph that is| + # |left aligned. | + # [+JUSTIFY+] Fully justified, words filled to width by spaces, + # except the last line. + # |A paragraph that| + # |is justified.| + # + # *Default*:: Text::Format::LEFT_ALIGN + # Used in:: #format, #paragraphs + attr_reader :format_style + + # Specifies the format style. Allowable values are: + # [+LEFT_ALIGN+] Left justified, ragged right. + # |A paragraph that is| + # |left aligned.| + # [+RIGHT_ALIGN+] Right justified, ragged left. + # |A paragraph that is| + # | right aligned.| + # [+RIGHT_FILL+] Left justified, right ragged, filled to width by + # spaces. (Essentially the same as +LEFT_ALIGN+ except + # that lines are padded on the right.) + # |A paragraph that is| + # |left aligned. | + # [+JUSTIFY+] Fully justified, words filled to width by spaces. + # |A paragraph that| + # |is justified.| + # + # *Default*:: Text::Format::LEFT_ALIGN + # Used in:: #format, #paragraphs + def format_style=(fs) + raise ArgumentError, "Invalid value provided for format_style." if ((fs < LEFT_ALIGN) || (fs > JUSTIFY)) + @format_style = fs + end + + # Indicates that the format style is left alignment. + # + # *Default*:: +true+ + # Used in:: #format, #paragraphs + def left_align? + return @format_style == LEFT_ALIGN + end + + # Indicates that the format style is right alignment. + # + # *Default*:: +false+ + # Used in:: #format, #paragraphs + def right_align? + return @format_style == RIGHT_ALIGN + end + + # Indicates that the format style is right fill. + # + # *Default*:: +false+ + # Used in:: #format, #paragraphs + def right_fill? + return @format_style == RIGHT_FILL + end + + # Indicates that the format style is full justification. + # + # *Default*:: +false+ + # Used in:: #format, #paragraphs + def justify? + return @format_style == JUSTIFY + end + + # The default implementation of #hyphenate_to implements + # SPLIT_CONTINUATION. + def hyphenate_to(word, size) + [word[0 .. (size - 2)] + "\\", word[(size - 1) .. -1]] + end + + private + def __do_split_word(word, size) #:nodoc: + [word[0 .. (size - 1)], word[size .. -1]] + end + + def __format(to_wrap) #:nodoc: + words = to_wrap.split(/\s+/).compact + words.shift if words[0].nil? or words[0].empty? + to_wrap = [] + + abbrev = false + width = @columns - @first_indent - @left_margin - @right_margin + indent_str = ' ' * @first_indent + first_line = true + line = words.shift + abbrev = __is_abbrev(line) unless line.nil? || line.empty? + + while w = words.shift + if (w.size + line.size < (width - 1)) || + ((line !~ LEQ_RE || abbrev) && (w.size + line.size < width)) + line << " " if (line =~ LEQ_RE) && (not abbrev) + line << " #{w}" + else + line, w = __do_break(line, w) if @nobreak + line, w = __do_hyphenate(line, w, width) if @hard_margins + if w.index(/\s+/) + w, *w2 = w.split(/\s+/) + words.unshift(w2) + words.flatten! + end + to_wrap << __make_line(line, indent_str, width, w.nil?) unless line.nil? + if first_line + first_line = false + width = @columns - @body_indent - @left_margin - @right_margin + indent_str = ' ' * @body_indent + end + line = w + end + + abbrev = __is_abbrev(w) unless w.nil? + end + + loop do + break if line.nil? or line.empty? + line, w = __do_hyphenate(line, w, width) if @hard_margins + to_wrap << __make_line(line, indent_str, width, w.nil?) + line = w + end + + if (@tag_paragraph && (to_wrap.size > 0)) then + clr = %r{`(\w+)'}.match([caller(1)].flatten[0])[1] + clr = "" if clr.nil? + + if ((not @tag_text[0].nil?) && (@tag_cur.size < 1) && + (clr != "__paragraphs")) then + @tag_cur = @tag_text[0] + end + + fchar = /(\S)/.match(to_wrap[0])[1] + white = to_wrap[0].index(fchar) + if ((white - @left_margin - 1) > @tag_cur.size) then + white = @tag_cur.size + @left_margin + to_wrap[0].gsub!(/^ {#{white}}/, "#{' ' * @left_margin}#{@tag_cur}") + else + to_wrap.unshift("#{' ' * @left_margin}#{@tag_cur}\n") + end + end + to_wrap.join('') + end + + # format lines in text into paragraphs with each element of @wrap a + # paragraph; uses Text::Format.format for the formatting + def __paragraphs(to_wrap) #:nodoc: + if ((@first_indent == @body_indent) || @tag_paragraph) then + p_end = "\n" + else + p_end = '' + end + + cnt = 0 + ret = [] + to_wrap.each do |tw| + @tag_cur = @tag_text[cnt] if @tag_paragraph + @tag_cur = '' if @tag_cur.nil? + line = __format(tw) + ret << "#{line}#{p_end}" if (not line.nil?) && (line.size > 0) + cnt += 1 + end + + ret[-1].chomp! unless ret.empty? + ret.join('') + end + + # center text using spaces on left side to pad it out empty lines + # are preserved + def __center(to_center) #:nodoc: + tabs = 0 + width = @columns - @left_margin - @right_margin + centered = [] + to_center.each do |tc| + s = tc.strip + tabs = s.count("\t") + tabs = 0 if tabs.nil? + ct = ((width - s.size - (tabs * @tabstop) + tabs) / 2) + ct = (width - @left_margin - @right_margin) - ct + centered << "#{s.rjust(ct)}\n" + end + centered.join('') + end + + # expand tabs to spaces should be similar to Text::Tabs::expand + def __expand(to_expand) #:nodoc: + expanded = [] + to_expand.split("\n").each { |te| expanded << te.gsub(/\t/, ' ' * @tabstop) } + expanded.join('') + end + + def __unexpand(to_unexpand) #:nodoc: + unexpanded = [] + to_unexpand.split("\n").each { |tu| unexpanded << tu.gsub(/ {#{@tabstop}}/, "\t") } + unexpanded.join('') + end + + def __is_abbrev(word) #:nodoc: + # remove period if there is one. + w = word.gsub(/\.$/, '') unless word.nil? + return true if (!@extra_space || ABBREV.include?(w) || @abbreviations.include?(w)) + false + end + + def __make_line(line, indent, width, last = false) #:nodoc: + lmargin = " " * @left_margin + fill = " " * (width - line.size) if right_fill? && (line.size <= width) + + if (justify? && ((not line.nil?) && (not line.empty?)) && line =~ /\S+\s+\S+/ && !last) + spaces = width - line.size + words = line.split(/(\s+)/) + ws = spaces / (words.size / 2) + spaces = spaces % (words.size / 2) if ws > 0 + words.reverse.each do |rw| + next if (rw =~ /^\S/) + rw.sub!(/^/, " " * ws) + next unless (spaces > 0) + rw.sub!(/^/, " ") + spaces -= 1 + end + line = words.join('') + end + line = "#{lmargin}#{indent}#{line}#{fill}\n" unless line.nil? + if right_align? && (not line.nil?) + line.sub(/^/, " " * (@columns - @right_margin - (line.size - 1))) + else + line + end + end + + def __do_hyphenate(line, next_line, width) #:nodoc: + rline = line.dup rescue line + rnext = next_line.dup rescue next_line + loop do + if rline.size == width + break + elsif rline.size > width + words = rline.strip.split(/\s+/) + word = words[-1].dup + size = width - rline.size + word.size + if (size <= 0) + words[-1] = nil + rline = words.join(' ').strip + rnext = "#{word} #{rnext}".strip + next + end + + first = rest = nil + + if ((@split_rules & SPLIT_HYPHENATION) != 0) + if @hyphenator_arity == 2 + first, rest = @hyphenator.hyphenate_to(word, size) + else + first, rest = @hyphenator.hyphenate_to(word, size, self) + end + end + + if ((@split_rules & SPLIT_CONTINUATION) != 0) and first.nil? + first, rest = self.hyphenate_to(word, size) + end + + if ((@split_rules & SPLIT_FIXED) != 0) and first.nil? + first.nil? or @split_rules == SPLIT_FIXED + first, rest = __do_split_word(word, size) + end + + if first.nil? + words[-1] = nil + rest = word + else + words[-1] = first + @split_words << SplitWord.new(word, first, rest) + end + rline = words.join(' ').strip + rnext = "#{rest} #{rnext}".strip + break + else + break if rnext.nil? or rnext.empty? or rline.nil? or rline.empty? + words = rnext.split(/\s+/) + word = words.shift + size = width - rline.size - 1 + + if (size <= 0) + rnext = "#{word} #{words.join(' ')}".strip + break + end + + first = rest = nil + + if ((@split_rules & SPLIT_HYPHENATION) != 0) + if @hyphenator_arity == 2 + first, rest = @hyphenator.hyphenate_to(word, size) + else + first, rest = @hyphenator.hyphenate_to(word, size, self) + end + end + + first, rest = self.hyphenate_to(word, size) if ((@split_rules & SPLIT_CONTINUATION) != 0) and first.nil? + + first, rest = __do_split_word(word, size) if ((@split_rules & SPLIT_FIXED) != 0) and first.nil? + + if (rline.size + (first ? first.size : 0)) < width + @split_words << SplitWord.new(word, first, rest) + rline = "#{rline} #{first}".strip + rnext = "#{rest} #{words.join(' ')}".strip + end + break + end + end + [rline, rnext] + end + + def __do_break(line, next_line) #:nodoc: + no_brk = false + words = [] + words = line.split(/\s+/) unless line.nil? + last_word = words[-1] + + @nobreak_regex.each { |k, v| no_brk = ((last_word =~ /#{k}/) and (next_line =~ /#{v}/)) } + + if no_brk && words.size > 1 + i = words.size + while i > 0 + no_brk = false + @nobreak_regex.each { |k, v| no_brk = ((words[i + 1] =~ /#{k}/) && (words[i] =~ /#{v}/)) } + i -= 1 + break if not no_brk + end + if i > 0 + l = brk_re(i).match(line) + line.sub!(brk_re(i), l[1]) + next_line = "#{l[2]} #{next_line}" + line.sub!(/\s+$/, '') + end + end + [line, next_line] + end + + def __create(arg = nil, &block) #:nodoc: + # Format::Text.new(text-to-wrap) + @text = arg unless arg.nil? + # Defaults + @columns = 72 + @tabstop = 8 + @first_indent = 4 + @body_indent = 0 + @format_style = LEFT_ALIGN + @left_margin = 0 + @right_margin = 0 + @extra_space = false + @text = Array.new if @text.nil? + @tag_paragraph = false + @tag_text = Array.new + @tag_cur = "" + @abbreviations = Array.new + @nobreak = false + @nobreak_regex = Hash.new + @split_words = Array.new + @hard_margins = false + @split_rules = SPLIT_FIXED + @hyphenator = self + @hyphenator_arity = self.method(:hyphenate_to).arity + + instance_eval(&block) unless block.nil? + end + + public + # Formats text into a nice paragraph format. The text is separated + # into words and then reassembled a word at a time using the settings + # of this Format object. If a word is larger than the number of + # columns available for formatting, then that word will appear on the + # line by itself. + # + # If +to_wrap+ is +nil+, then the value of #text will be + # worked on. + def format(to_wrap = nil) + to_wrap = @text if to_wrap.nil? + if to_wrap.class == Array + __format(to_wrap[0]) + else + __format(to_wrap) + end + end + + # Considers each element of text (provided or internal) as a paragraph. + # If #first_indent is the same as #body_indent, then + # paragraphs will be separated by a single empty line in the result; + # otherwise, the paragraphs will follow immediately after each other. + # Uses #format to do the heavy lifting. + def paragraphs(to_wrap = nil) + to_wrap = @text if to_wrap.nil? + __paragraphs([to_wrap].flatten) + end + + # Centers the text, preserving empty lines and tabs. + def center(to_center = nil) + to_center = @text if to_center.nil? + __center([to_center].flatten) + end + + # Replaces all tab characters in the text with #tabstop spaces. + def expand(to_expand = nil) + to_expand = @text if to_expand.nil? + if to_expand.class == Array + to_expand.collect { |te| __expand(te) } + else + __expand(to_expand) + end + end + + # Replaces all occurrences of #tabstop consecutive spaces + # with a tab character. + def unexpand(to_unexpand = nil) + to_unexpand = @text if to_unexpand.nil? + if to_unexpand.class == Array + to_unexpand.collect { |te| v << __unexpand(te) } + else + __unexpand(to_unexpand) + end + end + + # This constructor takes advantage of a technique for Ruby object + # construction introduced by Andy Hunt and Dave Thomas (see reference), + # where optional values are set using commands in a block. + # + # Text::Format.new { + # columns = 72 + # left_margin = 0 + # right_margin = 0 + # first_indent = 4 + # body_indent = 0 + # format_style = Text::Format::LEFT_ALIGN + # extra_space = false + # abbreviations = {} + # tag_paragraph = false + # tag_text = [] + # nobreak = false + # nobreak_regex = {} + # tabstop = 8 + # text = nil + # } + # + # As shown above, +arg+ is optional. If +arg+ is specified and is a + # +String+, then arg is used as the default value of #text. + # Alternately, an existing Text::Format object can be used or a Hash can + # be used. With all forms, a block can be specified. + # + # *Reference*:: "Object Construction and Blocks" + # + # + def initialize(arg = nil, &block) + case arg + when Text::Format + __create(arg.text) do + @columns = arg.columns + @tabstop = arg.tabstop + @first_indent = arg.first_indent + @body_indent = arg.body_indent + @format_style = arg.format_style + @left_margin = arg.left_margin + @right_margin = arg.right_margin + @extra_space = arg.extra_space + @tag_paragraph = arg.tag_paragraph + @tag_text = arg.tag_text + @abbreviations = arg.abbreviations + @nobreak = arg.nobreak + @nobreak_regex = arg.nobreak_regex + @text = arg.text + @hard_margins = arg.hard_margins + @split_words = arg.split_words + @split_rules = arg.split_rules + @hyphenator = arg.hyphenator + end + instance_eval(&block) unless block.nil? + when Hash + __create do + @columns = arg[:columns] || arg['columns'] || @columns + @tabstop = arg[:tabstop] || arg['tabstop'] || @tabstop + @first_indent = arg[:first_indent] || arg['first_indent'] || @first_indent + @body_indent = arg[:body_indent] || arg['body_indent'] || @body_indent + @format_style = arg[:format_style] || arg['format_style'] || @format_style + @left_margin = arg[:left_margin] || arg['left_margin'] || @left_margin + @right_margin = arg[:right_margin] || arg['right_margin'] || @right_margin + @extra_space = arg[:extra_space] || arg['extra_space'] || @extra_space + @text = arg[:text] || arg['text'] || @text + @tag_paragraph = arg[:tag_paragraph] || arg['tag_paragraph'] || @tag_paragraph + @tag_text = arg[:tag_text] || arg['tag_text'] || @tag_text + @abbreviations = arg[:abbreviations] || arg['abbreviations'] || @abbreviations + @nobreak = arg[:nobreak] || arg['nobreak'] || @nobreak + @nobreak_regex = arg[:nobreak_regex] || arg['nobreak_regex'] || @nobreak_regex + @hard_margins = arg[:hard_margins] || arg['hard_margins'] || @hard_margins + @split_rules = arg[:split_rules] || arg['split_rules'] || @split_rules + @hyphenator = arg[:hyphenator] || arg['hyphenator'] || @hyphenator + end + instance_eval(&block) unless block.nil? + when String + __create(arg, &block) + when NilClass + __create(&block) + else + raise TypeError + end + end + end +end + +if __FILE__ == $0 + require 'test/unit' + + class TestText__Format < Test::Unit::TestCase #:nodoc: + attr_accessor :format_o + + GETTYSBURG = <<-'EOS' + Four score and seven years ago our fathers brought forth on this + continent a new nation, conceived in liberty and dedicated to the + proposition that all men are created equal. Now we are engaged in + a great civil war, testing whether that nation or any nation so + conceived and so dedicated can long endure. We are met on a great + battlefield of that war. We have come to dedicate a portion of + that field as a final resting-place for those who here gave their + lives that that nation might live. It is altogether fitting and + proper that we should do this. But in a larger sense, we cannot + dedicate, we cannot consecrate, we cannot hallow this ground. + The brave men, living and dead who struggled here have consecrated + it far above our poor power to add or detract. The world will + little note nor long remember what we say here, but it can never + forget what they did here. It is for us the living rather to be + dedicated here to the unfinished work which they who fought here + have thus far so nobly advanced. It is rather for us to be here + dedicated to the great task remaining before us--that from these + honored dead we take increased devotion to that cause for which + they gave the last full measure of devotion--that we here highly + resolve that these dead shall not have died in vain, that this + nation under God shall have a new birth of freedom, and that + government of the people, by the people, for the people shall + not perish from the earth. + + -- Pres. Abraham Lincoln, 19 November 1863 + EOS + + FIVE_COL = "Four \nscore\nand s\neven \nyears\nago o\nur fa\nthers\nbroug\nht fo\nrth o\nn thi\ns con\ntinen\nt a n\new na\ntion,\nconce\nived \nin li\nberty\nand d\nedica\nted t\no the\npropo\nsitio\nn tha\nt all\nmen a\nre cr\neated\nequal\n. Now\nwe ar\ne eng\naged \nin a \ngreat\ncivil\nwar, \ntesti\nng wh\nether\nthat \nnatio\nn or \nany n\nation\nso co\nnceiv\ned an\nd so \ndedic\nated \ncan l\nong e\nndure\n. We \nare m\net on\na gre\nat ba\nttlef\nield \nof th\nat wa\nr. We\nhave \ncome \nto de\ndicat\ne a p\nortio\nn of \nthat \nfield\nas a \nfinal\nresti\nng-pl\nace f\nor th\nose w\nho he\nre ga\nve th\neir l\nives \nthat \nthat \nnatio\nn mig\nht li\nve. I\nt is \naltog\nether\nfitti\nng an\nd pro\nper t\nhat w\ne sho\nuld d\no thi\ns. Bu\nt in \na lar\nger s\nense,\nwe ca\nnnot \ndedic\nate, \nwe ca\nnnot \nconse\ncrate\n, we \ncanno\nt hal\nlow t\nhis g\nround\n. The\nbrave\nmen, \nlivin\ng and\ndead \nwho s\ntrugg\nled h\nere h\nave c\nonsec\nrated\nit fa\nr abo\nve ou\nr poo\nr pow\ner to\nadd o\nr det\nract.\nThe w\norld \nwill \nlittl\ne not\ne nor\nlong \nremem\nber w\nhat w\ne say\nhere,\nbut i\nt can\nnever\nforge\nt wha\nt the\ny did\nhere.\nIt is\nfor u\ns the\nlivin\ng rat\nher t\no be \ndedic\nated \nhere \nto th\ne unf\ninish\ned wo\nrk wh\nich t\nhey w\nho fo\nught \nhere \nhave \nthus \nfar s\no nob\nly ad\nvance\nd. It\nis ra\nther \nfor u\ns to \nbe he\nre de\ndicat\ned to\nthe g\nreat \ntask \nremai\nning \nbefor\ne us-\n-that\nfrom \nthese\nhonor\ned de\nad we\ntake \nincre\nased \ndevot\nion t\no tha\nt cau\nse fo\nr whi\nch th\ney ga\nve th\ne las\nt ful\nl mea\nsure \nof de\nvotio\nn--th\nat we\nhere \nhighl\ny res\nolve \nthat \nthese\ndead \nshall\nnot h\nave d\nied i\nn vai\nn, th\nat th\nis na\ntion \nunder\nGod s\nhall \nhave \na new\nbirth\nof fr\needom\n, and\nthat \ngover\nnment\nof th\ne peo\nple, \nby th\ne peo\nple, \nfor t\nhe pe\nople \nshall\nnot p\nerish\nfrom \nthe e\narth.\n-- Pr\nes. A\nbraha\nm Lin\ncoln,\n19 No\nvembe\nr 186\n3 \n" + + FIVE_CNT = "Four \nscore\nand \nseven\nyears\nago \nour \nfath\\\ners \nbrou\\\nght \nforth\non t\\\nhis \ncont\\\ninent\na new\nnati\\\non, \nconc\\\neived\nin l\\\niber\\\nty a\\\nnd d\\\nedic\\\nated \nto t\\\nhe p\\\nropo\\\nsiti\\\non t\\\nhat \nall \nmen \nare \ncrea\\\nted \nequa\\\nl. N\\\now we\nare \nenga\\\nged \nin a \ngreat\ncivil\nwar, \ntest\\\ning \nwhet\\\nher \nthat \nnati\\\non or\nany \nnati\\\non so\nconc\\\neived\nand \nso d\\\nedic\\\nated \ncan \nlong \nendu\\\nre. \nWe a\\\nre m\\\net on\na gr\\\neat \nbatt\\\nlefi\\\neld \nof t\\\nhat \nwar. \nWe h\\\nave \ncome \nto d\\\nedic\\\nate a\nport\\\nion \nof t\\\nhat \nfield\nas a \nfinal\nrest\\\ning-\\\nplace\nfor \nthose\nwho \nhere \ngave \ntheir\nlives\nthat \nthat \nnati\\\non m\\\night \nlive.\nIt is\nalto\\\ngeth\\\ner f\\\nitti\\\nng a\\\nnd p\\\nroper\nthat \nwe s\\\nhould\ndo t\\\nhis. \nBut \nin a \nlarg\\\ner s\\\nense,\nwe c\\\nannot\ndedi\\\ncate,\nwe c\\\nannot\ncons\\\necra\\\nte, \nwe c\\\nannot\nhall\\\now t\\\nhis \ngrou\\\nnd. \nThe \nbrave\nmen, \nlivi\\\nng a\\\nnd d\\\nead \nwho \nstru\\\nggled\nhere \nhave \ncons\\\necra\\\nted \nit f\\\nar a\\\nbove \nour \npoor \npower\nto a\\\ndd or\ndetr\\\nact. \nThe \nworld\nwill \nlitt\\\nle n\\\note \nnor \nlong \nreme\\\nmber \nwhat \nwe s\\\nay h\\\nere, \nbut \nit c\\\nan n\\\never \nforg\\\net w\\\nhat \nthey \ndid \nhere.\nIt is\nfor \nus t\\\nhe l\\\niving\nrath\\\ner to\nbe d\\\nedic\\\nated \nhere \nto t\\\nhe u\\\nnfin\\\nished\nwork \nwhich\nthey \nwho \nfoug\\\nht h\\\nere \nhave \nthus \nfar \nso n\\\nobly \nadva\\\nnced.\nIt is\nrath\\\ner f\\\nor us\nto be\nhere \ndedi\\\ncated\nto t\\\nhe g\\\nreat \ntask \nrema\\\nining\nbefo\\\nre u\\\ns--t\\\nhat \nfrom \nthese\nhono\\\nred \ndead \nwe t\\\nake \nincr\\\neased\ndevo\\\ntion \nto t\\\nhat \ncause\nfor \nwhich\nthey \ngave \nthe \nlast \nfull \nmeas\\\nure \nof d\\\nevot\\\nion-\\\n-that\nwe h\\\nere \nhigh\\\nly r\\\nesol\\\nve t\\\nhat \nthese\ndead \nshall\nnot \nhave \ndied \nin v\\\nain, \nthat \nthis \nnati\\\non u\\\nnder \nGod \nshall\nhave \na new\nbirth\nof f\\\nreed\\\nom, \nand \nthat \ngove\\\nrnme\\\nnt of\nthe \npeop\\\nle, \nby t\\\nhe p\\\neopl\\\ne, f\\\nor t\\\nhe p\\\neople\nshall\nnot \nperi\\\nsh f\\\nrom \nthe \neart\\\nh. --\nPres.\nAbra\\\nham \nLinc\\\noln, \n19 N\\\novem\\\nber \n1863 \n" + + # Tests both abbreviations and abbreviations= + def test_abbreviations + abbr = [" Pres. Abraham Lincoln\n", " Pres. Abraham Lincoln\n"] + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal([], @format_o.abbreviations) + assert_nothing_raised { @format_o.abbreviations = [ 'foo', 'bar' ] } + assert_equal([ 'foo', 'bar' ], @format_o.abbreviations) + assert_equal(abbr[0], @format_o.format(abbr[0])) + assert_nothing_raised { @format_o.extra_space = true } + assert_equal(abbr[1], @format_o.format(abbr[0])) + assert_nothing_raised { @format_o.abbreviations = [ "Pres" ] } + assert_equal([ "Pres" ], @format_o.abbreviations) + assert_equal(abbr[0], @format_o.format(abbr[0])) + assert_nothing_raised { @format_o.extra_space = false } + assert_equal(abbr[0], @format_o.format(abbr[0])) + end + + # Tests both body_indent and body_indent= + def test_body_indent + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(0, @format_o.body_indent) + assert_nothing_raised { @format_o.body_indent = 7 } + assert_equal(7, @format_o.body_indent) + assert_nothing_raised { @format_o.body_indent = -3 } + assert_equal(3, @format_o.body_indent) + assert_nothing_raised { @format_o.body_indent = "9" } + assert_equal(9, @format_o.body_indent) + assert_nothing_raised { @format_o.body_indent = "-2" } + assert_equal(2, @format_o.body_indent) + assert_match(/^ [^ ]/, @format_o.format(GETTYSBURG).split("\n")[1]) + end + + # Tests both columns and columns= + def test_columns + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(72, @format_o.columns) + assert_nothing_raised { @format_o.columns = 7 } + assert_equal(7, @format_o.columns) + assert_nothing_raised { @format_o.columns = -3 } + assert_equal(3, @format_o.columns) + assert_nothing_raised { @format_o.columns = "9" } + assert_equal(9, @format_o.columns) + assert_nothing_raised { @format_o.columns = "-2" } + assert_equal(2, @format_o.columns) + assert_nothing_raised { @format_o.columns = 40 } + assert_equal(40, @format_o.columns) + assert_match(/this continent$/, + @format_o.format(GETTYSBURG).split("\n")[1]) + end + + # Tests both extra_space and extra_space= + def test_extra_space + assert_nothing_raised { @format_o = Text::Format.new } + assert(!@format_o.extra_space) + assert_nothing_raised { @format_o.extra_space = true } + assert(@format_o.extra_space) + # The behaviour of extra_space is tested in test_abbreviations. There + # is no need to reproduce it here. + end + + # Tests both first_indent and first_indent= + def test_first_indent + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(4, @format_o.first_indent) + assert_nothing_raised { @format_o.first_indent = 7 } + assert_equal(7, @format_o.first_indent) + assert_nothing_raised { @format_o.first_indent = -3 } + assert_equal(3, @format_o.first_indent) + assert_nothing_raised { @format_o.first_indent = "9" } + assert_equal(9, @format_o.first_indent) + assert_nothing_raised { @format_o.first_indent = "-2" } + assert_equal(2, @format_o.first_indent) + assert_match(/^ [^ ]/, @format_o.format(GETTYSBURG).split("\n")[0]) + end + + def test_format_style + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(Text::Format::LEFT_ALIGN, @format_o.format_style) + assert_match(/^November 1863$/, + @format_o.format(GETTYSBURG).split("\n")[-1]) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_ALIGN + } + assert_equal(Text::Format::RIGHT_ALIGN, @format_o.format_style) + assert_match(/^ +November 1863$/, + @format_o.format(GETTYSBURG).split("\n")[-1]) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_FILL + } + assert_equal(Text::Format::RIGHT_FILL, @format_o.format_style) + assert_match(/^November 1863 +$/, + @format_o.format(GETTYSBURG).split("\n")[-1]) + assert_nothing_raised { @format_o.format_style = Text::Format::JUSTIFY } + assert_equal(Text::Format::JUSTIFY, @format_o.format_style) + assert_match(/^of freedom, and that government of the people, by the people, for the$/, + @format_o.format(GETTYSBURG).split("\n")[-3]) + assert_raises(ArgumentError) { @format_o.format_style = 33 } + end + + def test_tag_paragraph + assert_nothing_raised { @format_o = Text::Format.new } + assert(!@format_o.tag_paragraph) + assert_nothing_raised { @format_o.tag_paragraph = true } + assert(@format_o.tag_paragraph) + assert_not_equal(@format_o.paragraphs([GETTYSBURG, GETTYSBURG]), + Text::Format.new.paragraphs([GETTYSBURG, GETTYSBURG])) + end + + def test_tag_text + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal([], @format_o.tag_text) + assert_equal(@format_o.format(GETTYSBURG), + Text::Format.new.format(GETTYSBURG)) + assert_nothing_raised { + @format_o.tag_paragraph = true + @format_o.tag_text = ["Gettysburg Address", "---"] + } + assert_not_equal(@format_o.format(GETTYSBURG), + Text::Format.new.format(GETTYSBURG)) + assert_not_equal(@format_o.paragraphs([GETTYSBURG, GETTYSBURG]), + Text::Format.new.paragraphs([GETTYSBURG, GETTYSBURG])) + assert_not_equal(@format_o.paragraphs([GETTYSBURG, GETTYSBURG, + GETTYSBURG]), + Text::Format.new.paragraphs([GETTYSBURG, GETTYSBURG, + GETTYSBURG])) + end + + def test_justify? + assert_nothing_raised { @format_o = Text::Format.new } + assert(!@format_o.justify?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_ALIGN + } + assert(!@format_o.justify?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_FILL + } + assert(!@format_o.justify?) + assert_nothing_raised { + @format_o.format_style = Text::Format::JUSTIFY + } + assert(@format_o.justify?) + # The format testing is done in test_format_style + end + + def test_left_align? + assert_nothing_raised { @format_o = Text::Format.new } + assert(@format_o.left_align?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_ALIGN + } + assert(!@format_o.left_align?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_FILL + } + assert(!@format_o.left_align?) + assert_nothing_raised { @format_o.format_style = Text::Format::JUSTIFY } + assert(!@format_o.left_align?) + # The format testing is done in test_format_style + end + + def test_left_margin + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(0, @format_o.left_margin) + assert_nothing_raised { @format_o.left_margin = -3 } + assert_equal(3, @format_o.left_margin) + assert_nothing_raised { @format_o.left_margin = "9" } + assert_equal(9, @format_o.left_margin) + assert_nothing_raised { @format_o.left_margin = "-2" } + assert_equal(2, @format_o.left_margin) + assert_nothing_raised { @format_o.left_margin = 7 } + assert_equal(7, @format_o.left_margin) + assert_nothing_raised { + ft = @format_o.format(GETTYSBURG).split("\n") + assert_match(/^ {11}Four score/, ft[0]) + assert_match(/^ {7}November/, ft[-1]) + } + end + + def test_hard_margins + assert_nothing_raised { @format_o = Text::Format.new } + assert(!@format_o.hard_margins) + assert_nothing_raised { + @format_o.hard_margins = true + @format_o.columns = 5 + @format_o.first_indent = 0 + @format_o.format_style = Text::Format::RIGHT_FILL + } + assert(@format_o.hard_margins) + assert_equal(FIVE_COL, @format_o.format(GETTYSBURG)) + assert_nothing_raised { + @format_o.split_rules |= Text::Format::SPLIT_CONTINUATION + assert_equal(Text::Format::SPLIT_CONTINUATION_FIXED, + @format_o.split_rules) + } + assert_equal(FIVE_CNT, @format_o.format(GETTYSBURG)) + end + + # Tests both nobreak and nobreak_regex, since one is only useful + # with the other. + def test_nobreak + assert_nothing_raised { @format_o = Text::Format.new } + assert(!@format_o.nobreak) + assert(@format_o.nobreak_regex.empty?) + assert_nothing_raised { + @format_o.nobreak = true + @format_o.nobreak_regex = { '^this$' => '^continent$' } + @format_o.columns = 77 + } + assert(@format_o.nobreak) + assert_equal({ '^this$' => '^continent$' }, @format_o.nobreak_regex) + assert_match(/^this continent/, + @format_o.format(GETTYSBURG).split("\n")[1]) + end + + def test_right_align? + assert_nothing_raised { @format_o = Text::Format.new } + assert(!@format_o.right_align?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_ALIGN + } + assert(@format_o.right_align?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_FILL + } + assert(!@format_o.right_align?) + assert_nothing_raised { @format_o.format_style = Text::Format::JUSTIFY } + assert(!@format_o.right_align?) + # The format testing is done in test_format_style + end + + def test_right_fill? + assert_nothing_raised { @format_o = Text::Format.new } + assert(!@format_o.right_fill?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_ALIGN + } + assert(!@format_o.right_fill?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_FILL + } + assert(@format_o.right_fill?) + assert_nothing_raised { + @format_o.format_style = Text::Format::JUSTIFY + } + assert(!@format_o.right_fill?) + # The format testing is done in test_format_style + end + + def test_right_margin + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(0, @format_o.right_margin) + assert_nothing_raised { @format_o.right_margin = -3 } + assert_equal(3, @format_o.right_margin) + assert_nothing_raised { @format_o.right_margin = "9" } + assert_equal(9, @format_o.right_margin) + assert_nothing_raised { @format_o.right_margin = "-2" } + assert_equal(2, @format_o.right_margin) + assert_nothing_raised { @format_o.right_margin = 7 } + assert_equal(7, @format_o.right_margin) + assert_nothing_raised { + ft = @format_o.format(GETTYSBURG).split("\n") + assert_match(/^ {4}Four score.*forth on$/, ft[0]) + assert_match(/^November/, ft[-1]) + } + end + + def test_tabstop + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(8, @format_o.tabstop) + assert_nothing_raised { @format_o.tabstop = 7 } + assert_equal(7, @format_o.tabstop) + assert_nothing_raised { @format_o.tabstop = -3 } + assert_equal(3, @format_o.tabstop) + assert_nothing_raised { @format_o.tabstop = "9" } + assert_equal(9, @format_o.tabstop) + assert_nothing_raised { @format_o.tabstop = "-2" } + assert_equal(2, @format_o.tabstop) + end + + def test_text + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal([], @format_o.text) + assert_nothing_raised { @format_o.text = "Test Text" } + assert_equal("Test Text", @format_o.text) + assert_nothing_raised { @format_o.text = ["Line 1", "Line 2"] } + assert_equal(["Line 1", "Line 2"], @format_o.text) + end + + def test_s_new + # new(NilClass) { block } + assert_nothing_raised do + @format_o = Text::Format.new { + self.text = "Test 1, 2, 3" + } + end + assert_equal("Test 1, 2, 3", @format_o.text) + + # new(Hash Symbols) + assert_nothing_raised { @format_o = Text::Format.new(:columns => 72) } + assert_equal(72, @format_o.columns) + + # new(Hash String) + assert_nothing_raised { @format_o = Text::Format.new('columns' => 72) } + assert_equal(72, @format_o.columns) + + # new(Hash) { block } + assert_nothing_raised do + @format_o = Text::Format.new('columns' => 80) { + self.text = "Test 4, 5, 6" + } + end + assert_equal("Test 4, 5, 6", @format_o.text) + assert_equal(80, @format_o.columns) + + # new(Text::Format) + assert_nothing_raised do + fo = Text::Format.new(@format_o) + assert(fo == @format_o) + end + + # new(Text::Format) { block } + assert_nothing_raised do + fo = Text::Format.new(@format_o) { self.columns = 79 } + assert(fo != @format_o) + end + + # new(String) + assert_nothing_raised { @format_o = Text::Format.new("Test A, B, C") } + assert_equal("Test A, B, C", @format_o.text) + + # new(String) { block } + assert_nothing_raised do + @format_o = Text::Format.new("Test X, Y, Z") { self.columns = -5 } + end + assert_equal("Test X, Y, Z", @format_o.text) + assert_equal(5, @format_o.columns) + end + + def test_center + assert_nothing_raised { @format_o = Text::Format.new } + assert_nothing_raised do + ct = @format_o.center(GETTYSBURG.split("\n")).split("\n") + assert_match(/^ Four score and seven years ago our fathers brought forth on this/, ct[0]) + assert_match(/^ not perish from the earth./, ct[-3]) + end + end + + def test_expand + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(" ", @format_o.expand("\t ")) + assert_nothing_raised { @format_o.tabstop = 4 } + assert_equal(" ", @format_o.expand("\t ")) + end + + def test_unexpand + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal("\t ", @format_o.unexpand(" ")) + assert_nothing_raised { @format_o.tabstop = 4 } + assert_equal("\t ", @format_o.unexpand(" ")) + end + + def test_space_only + assert_equal("", Text::Format.new.format(" ")) + assert_equal("", Text::Format.new.format("\n")) + assert_equal("", Text::Format.new.format(" ")) + assert_equal("", Text::Format.new.format(" \n")) + assert_equal("", Text::Format.new.paragraphs("\n")) + assert_equal("", Text::Format.new.paragraphs(" ")) + assert_equal("", Text::Format.new.paragraphs(" ")) + assert_equal("", Text::Format.new.paragraphs(" \n")) + assert_equal("", Text::Format.new.paragraphs(["\n"])) + assert_equal("", Text::Format.new.paragraphs([" "])) + assert_equal("", Text::Format.new.paragraphs([" "])) + assert_equal("", Text::Format.new.paragraphs([" \n"])) + end + + def test_splendiferous + h = nil + test = "This is a splendiferous test" + assert_nothing_raised { @format_o = Text::Format.new(:columns => 6, :left_margin => 0, :indent => 0, :first_indent => 0) } + assert_match(/^splendiferous$/, @format_o.format(test)) + assert_nothing_raised { @format_o.hard_margins = true } + assert_match(/^lendif$/, @format_o.format(test)) + assert_nothing_raised { h = Object.new } + assert_nothing_raised do + @format_o.split_rules = Text::Format::SPLIT_HYPHENATION + class << h #:nodoc: + def hyphenate_to(word, size) + return ["", word] if size < 2 + [word[0 ... size], word[size .. -1]] + end + end + @format_o.hyphenator = h + end + assert_match(/^iferou$/, @format_o.format(test)) + assert_nothing_raised { h = Object.new } + assert_nothing_raised do + class << h #:nodoc: + def hyphenate_to(word, size, formatter) + return ["", word] if word.size < formatter.columns + [word[0 ... size], word[size .. -1]] + end + end + @format_o.hyphenator = h + end + assert_match(/^ferous$/, @format_o.format(test)) + end + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail.rb new file mode 100755 index 00000000..8cea88d3 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail.rb @@ -0,0 +1,3 @@ +require 'tmail/info' +require 'tmail/mail' +require 'tmail/mailbox' diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/address.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/address.rb new file mode 100755 index 00000000..235ec761 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/address.rb @@ -0,0 +1,242 @@ +# +# address.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/encode' +require 'tmail/parser' + + +module TMail + + class Address + + include TextUtils + + def Address.parse( str ) + Parser.parse :ADDRESS, str + end + + def address_group? + false + end + + def initialize( local, domain ) + if domain + domain.each do |s| + raise SyntaxError, 'empty word in domain' if s.empty? + end + end + @local = local + @domain = domain + @name = nil + @routes = [] + end + + attr_reader :name + + def name=( str ) + @name = str + @name = nil if str and str.empty? + end + + alias phrase name + alias phrase= name= + + attr_reader :routes + + def inspect + "#<#{self.class} #{address()}>" + end + + def local + return nil unless @local + return '""' if @local.size == 1 and @local[0].empty? + @local.map {|i| quote_atom(i) }.join('.') + end + + def domain + return nil unless @domain + join_domain(@domain) + end + + def spec + s = self.local + d = self.domain + if s and d + s + '@' + d + else + s + end + end + + alias address spec + + + def ==( other ) + other.respond_to? :spec and self.spec == other.spec + end + + alias eql? == + + def hash + @local.hash ^ @domain.hash + end + + def dup + obj = self.class.new(@local.dup, @domain.dup) + obj.name = @name.dup if @name + obj.routes.replace @routes + obj + end + + include StrategyInterface + + def accept( strategy, dummy1 = nil, dummy2 = nil ) + unless @local + strategy.meta '<>' # empty return-path + return + end + + spec_p = (not @name and @routes.empty?) + if @name + strategy.phrase @name + strategy.space + end + tmp = spec_p ? '' : '<' + unless @routes.empty? + tmp << @routes.map {|i| '@' + i }.join(',') << ':' + end + tmp << self.spec + tmp << '>' unless spec_p + strategy.meta tmp + strategy.lwsp '' + end + + end + + + class AddressGroup + + include Enumerable + + def address_group? + true + end + + def initialize( name, addrs ) + @name = name + @addresses = addrs + end + + attr_reader :name + + def ==( other ) + other.respond_to? :to_a and @addresses == other.to_a + end + + alias eql? == + + def hash + map {|i| i.hash }.hash + end + + def []( idx ) + @addresses[idx] + end + + def size + @addresses.size + end + + def empty? + @addresses.empty? + end + + def each( &block ) + @addresses.each(&block) + end + + def to_a + @addresses.dup + end + + alias to_ary to_a + + def include?( a ) + @addresses.include? a + end + + def flatten + set = [] + @addresses.each do |a| + if a.respond_to? :flatten + set.concat a.flatten + else + set.push a + end + end + set + end + + def each_address( &block ) + flatten.each(&block) + end + + def add( a ) + @addresses.push a + end + + alias push add + + def delete( a ) + @addresses.delete a + end + + include StrategyInterface + + def accept( strategy, dummy1 = nil, dummy2 = nil ) + strategy.phrase @name + strategy.meta ':' + strategy.space + first = true + each do |mbox| + if first + first = false + else + strategy.meta ',' + end + strategy.space + mbox.accept strategy + end + strategy.meta ';' + strategy.lwsp '' + end + + end + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/attachments.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/attachments.rb new file mode 100644 index 00000000..4d8d106a --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/attachments.rb @@ -0,0 +1,39 @@ +require 'stringio' + +module TMail + class Attachment < StringIO + attr_accessor :original_filename, :content_type + end + + class Mail + def has_attachments? + multipart? && parts.any? { |part| attachment?(part) } + end + + def attachment?(part) + (part['content-disposition'] && part['content-disposition'].disposition == "attachment") || + part.header['content-type'].main_type != "text" + end + + def attachments + if multipart? + parts.collect { |part| + if attachment?(part) + content = part.body # unquoted automatically by TMail#body + file_name = (part['content-location'] && + part['content-location'].body) || + part.sub_header("content-type", "name") || + part.sub_header("content-disposition", "filename") + + next if file_name.blank? || content.blank? + + attachment = Attachment.new(content) + attachment.original_filename = file_name.strip + attachment.content_type = part.content_type + attachment + end + }.compact + end + end + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/base64.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/base64.rb new file mode 100755 index 00000000..8f89a489 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/base64.rb @@ -0,0 +1,71 @@ +# +# base64.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +module TMail + + module Base64 + + module_function + + def rb_folding_encode( str, eol = "\n", limit = 60 ) + [str].pack('m') + end + + def rb_encode( str ) + [str].pack('m').tr( "\r\n", '' ) + end + + def rb_decode( str, strict = false ) + str.unpack('m') + end + + begin + require 'tmail/base64.so' + alias folding_encode c_folding_encode + alias encode c_encode + alias decode c_decode + class << self + alias folding_encode c_folding_encode + alias encode c_encode + alias decode c_decode + end + rescue LoadError + alias folding_encode rb_folding_encode + alias encode rb_encode + alias decode rb_decode + class << self + alias folding_encode rb_folding_encode + alias encode rb_encode + alias decode rb_decode + end + end + + end + +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/config.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/config.rb new file mode 100755 index 00000000..b075299b --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/config.rb @@ -0,0 +1,69 @@ +# +# config.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +module TMail + + class Config + + def initialize( strict ) + @strict_parse = strict + @strict_base64decode = strict + end + + def strict_parse? + @strict_parse + end + + attr_writer :strict_parse + + def strict_base64decode? + @strict_base64decode + end + + attr_writer :strict_base64decode + + def new_body_port( mail ) + StringPort.new + end + + alias new_preamble_port new_body_port + alias new_part_port new_body_port + + end + + DEFAULT_CONFIG = Config.new(false) + DEFAULT_STRICT_CONFIG = Config.new(true) + + def Config.to_config( arg ) + return DEFAULT_STRICT_CONFIG if arg == true + return DEFAULT_CONFIG if arg == false + arg or DEFAULT_CONFIG + end + +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/encode.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/encode.rb new file mode 100755 index 00000000..91bd289c --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/encode.rb @@ -0,0 +1,467 @@ +# +# encode.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'nkf' +require 'tmail/base64.rb' +require 'tmail/stringio' +require 'tmail/utils' + + +module TMail + + module StrategyInterface + + def create_dest( obj ) + case obj + when nil + StringOutput.new + when String + StringOutput.new(obj) + when IO, StringOutput + obj + else + raise TypeError, 'cannot handle this type of object for dest' + end + end + module_function :create_dest + + def encoded( eol = "\r\n", charset = 'j', dest = nil ) + accept_strategy Encoder, eol, charset, dest + end + + def decoded( eol = "\n", charset = 'e', dest = nil ) + accept_strategy Decoder, eol, charset, dest + end + + alias to_s decoded + + def accept_strategy( klass, eol, charset, dest = nil ) + dest ||= '' + accept klass.new(create_dest(dest), charset, eol) + dest + end + + end + + + ### + ### MIME B encoding decoder + ### + + class Decoder + + include TextUtils + + encoded = '=\?(?:iso-2022-jp|euc-jp|shift_jis)\?[QB]\?[a-z0-9+/=]+\?=' + ENCODED_WORDS = /#{encoded}(?:\s+#{encoded})*/i + + OUTPUT_ENCODING = { + 'EUC' => 'e', + 'SJIS' => 's', + } + + def self.decode( str, encoding = nil ) + encoding ||= (OUTPUT_ENCODING[$KCODE] || 'j') + opt = '-m' + encoding + str.gsub(ENCODED_WORDS) {|s| NKF.nkf(opt, s) } + end + + def initialize( dest, encoding = nil, eol = "\n" ) + @f = StrategyInterface.create_dest(dest) + @encoding = (/\A[ejs]/ === encoding) ? encoding[0,1] : nil + @eol = eol + end + + def decode( str ) + self.class.decode(str, @encoding) + end + private :decode + + def terminate + end + + def header_line( str ) + @f << decode(str) + end + + def header_name( nm ) + @f << nm << ': ' + end + + def header_body( str ) + @f << decode(str) + end + + def space + @f << ' ' + end + + alias spc space + + def lwsp( str ) + @f << str + end + + def meta( str ) + @f << str + end + + def text( str ) + @f << decode(str) + end + + def phrase( str ) + @f << quote_phrase(decode(str)) + end + + def kv_pair( k, v ) + @f << k << '=' << v + end + + def puts( str = nil ) + @f << str if str + @f << @eol + end + + def write( str ) + @f << str + end + + end + + + ### + ### MIME B-encoding encoder + ### + + # + # FIXME: This class can handle only (euc-jp/shift_jis -> iso-2022-jp). + # + class Encoder + + include TextUtils + + BENCODE_DEBUG = false unless defined?(BENCODE_DEBUG) + + def Encoder.encode( str ) + e = new() + e.header_body str + e.terminate + e.dest.string + end + + SPACER = "\t" + MAX_LINE_LEN = 70 + + OPTIONS = { + 'EUC' => '-Ej -m0', + 'SJIS' => '-Sj -m0', + 'UTF8' => nil, # FIXME + 'NONE' => nil + } + + def initialize( dest = nil, encoding = nil, eol = "\r\n", limit = nil ) + @f = StrategyInterface.create_dest(dest) + @opt = OPTIONS[$KCODE] + @eol = eol + reset + end + + def normalize_encoding( str ) + if @opt + then NKF.nkf(@opt, str) + else str + end + end + + def reset + @text = '' + @lwsp = '' + @curlen = 0 + end + + def terminate + add_lwsp '' + reset + end + + def dest + @f + end + + def puts( str = nil ) + @f << str if str + @f << @eol + end + + def write( str ) + @f << str + end + + # + # add + # + + def header_line( line ) + scanadd line + end + + def header_name( name ) + add_text name.split(/-/).map {|i| i.capitalize }.join('-') + add_text ':' + add_lwsp ' ' + end + + def header_body( str ) + scanadd normalize_encoding(str) + end + + def space + add_lwsp ' ' + end + + alias spc space + + def lwsp( str ) + add_lwsp str.sub(/[\r\n]+[^\r\n]*\z/, '') + end + + def meta( str ) + add_text str + end + + def text( str ) + scanadd normalize_encoding(str) + end + + def phrase( str ) + str = normalize_encoding(str) + if CONTROL_CHAR === str + scanadd str + else + add_text quote_phrase(str) + end + end + + # FIXME: implement line folding + # + def kv_pair( k, v ) + return if v.nil? + v = normalize_encoding(v) + if token_safe?(v) + add_text k + '=' + v + elsif not CONTROL_CHAR === v + add_text k + '=' + quote_token(v) + else + # apply RFC2231 encoding + kv = k + '*=' + "iso-2022-jp'ja'" + encode_value(v) + add_text kv + end + end + + def encode_value( str ) + str.gsub(TOKEN_UNSAFE) {|s| '%%%02x' % s[0] } + end + + private + + def scanadd( str, force = false ) + types = '' + strs = [] + + until str.empty? + if m = /\A[^\e\t\r\n ]+/.match(str) + types << (force ? 'j' : 'a') + strs.push m[0] + + elsif m = /\A[\t\r\n ]+/.match(str) + types << 's' + strs.push m[0] + + elsif m = /\A\e../.match(str) + esc = m[0] + str = m.post_match + if esc != "\e(B" and m = /\A[^\e]+/.match(str) + types << 'j' + strs.push m[0] + end + + else + raise 'TMail FATAL: encoder scan fail' + end + (str = m.post_match) unless m.nil? + end + + do_encode types, strs + end + + def do_encode( types, strs ) + # + # result : (A|E)(S(A|E))* + # E : W(SW)* + # W : (J|A)+ but must contain J # (J|A)*J(J|A)* + # A : <> + # J : <> + # S : <> + # + # An encoding unit is `E'. + # Input (parameter `types') is (J|A)(J|A|S)*(J|A) + # + if BENCODE_DEBUG + puts + puts '-- do_encode ------------' + puts types.split(//).join(' ') + p strs + end + + e = /[ja]*j[ja]*(?:s[ja]*j[ja]*)*/ + + while m = e.match(types) + pre = m.pre_match + concat_A_S pre, strs[0, pre.size] unless pre.empty? + concat_E m[0], strs[m.begin(0) ... m.end(0)] + types = m.post_match + strs.slice! 0, m.end(0) + end + concat_A_S types, strs + end + + def concat_A_S( types, strs ) + i = 0 + types.each_byte do |t| + case t + when ?a then add_text strs[i] + when ?s then add_lwsp strs[i] + else + raise "TMail FATAL: unknown flag: #{t.chr}" + end + i += 1 + end + end + + METHOD_ID = { + ?j => :extract_J, + ?e => :extract_E, + ?a => :extract_A, + ?s => :extract_S + } + + def concat_E( types, strs ) + if BENCODE_DEBUG + puts '---- concat_E' + puts "types=#{types.split(//).join(' ')}" + puts "strs =#{strs.inspect}" + end + + flush() unless @text.empty? + + chunk = '' + strs.each_with_index do |s,i| + mid = METHOD_ID[types[i]] + until s.empty? + unless c = __send__(mid, chunk.size, s) + add_with_encode chunk unless chunk.empty? + flush + chunk = '' + fold + c = __send__(mid, 0, s) + raise 'TMail FATAL: extract fail' unless c + end + chunk << c + end + end + add_with_encode chunk unless chunk.empty? + end + + def extract_J( chunksize, str ) + size = max_bytes(chunksize, str.size) - 6 + size = (size % 2 == 0) ? (size) : (size - 1) + return nil if size <= 0 + "\e$B#{str.slice!(0, size)}\e(B" + end + + def extract_A( chunksize, str ) + size = max_bytes(chunksize, str.size) + return nil if size <= 0 + str.slice!(0, size) + end + + alias extract_S extract_A + + def max_bytes( chunksize, ssize ) + (restsize() - '=?iso-2022-jp?B??='.size) / 4 * 3 - chunksize + end + + # + # free length buffer + # + + def add_text( str ) + @text << str + # puts '---- text -------------------------------------' + # puts "+ #{str.inspect}" + # puts "txt >>>#{@text.inspect}<<<" + end + + def add_with_encode( str ) + @text << "=?iso-2022-jp?B?#{Base64.encode(str)}?=" + end + + def add_lwsp( lwsp ) + # puts '---- lwsp -------------------------------------' + # puts "+ #{lwsp.inspect}" + fold if restsize() <= 0 + flush + @lwsp = lwsp + end + + def flush + # puts '---- flush ----' + # puts "spc >>>#{@lwsp.inspect}<<<" + # puts "txt >>>#{@text.inspect}<<<" + @f << @lwsp << @text + @curlen += (@lwsp.size + @text.size) + @text = '' + @lwsp = '' + end + + def fold + # puts '---- fold ----' + @f << @eol + @curlen = 0 + @lwsp = SPACER + end + + def restsize + MAX_LINE_LEN - (@curlen + @lwsp.size + @text.size) + end + + end + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/facade.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/facade.rb new file mode 100755 index 00000000..1ecd64bf --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/facade.rb @@ -0,0 +1,552 @@ +# +# facade.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/utils' + + +module TMail + + class Mail + + def header_string( name, default = nil ) + h = @header[name.downcase] or return default + h.to_s + end + + ### + ### attributes + ### + + include TextUtils + + def set_string_array_attr( key, strs ) + strs.flatten! + if strs.empty? + @header.delete key.downcase + else + store key, strs.join(', ') + end + strs + end + private :set_string_array_attr + + def set_string_attr( key, str ) + if str + store key, str + else + @header.delete key.downcase + end + str + end + private :set_string_attr + + def set_addrfield( name, arg ) + if arg + h = HeaderField.internal_new(name, @config) + h.addrs.replace [arg].flatten + @header[name] = h + else + @header.delete name + end + arg + end + private :set_addrfield + + def addrs2specs( addrs ) + return nil unless addrs + list = addrs.map {|addr| + if addr.address_group? + then addr.map {|a| a.spec } + else addr.spec + end + }.flatten + return nil if list.empty? + list + end + private :addrs2specs + + + # + # date time + # + + def date( default = nil ) + if h = @header['date'] + h.date + else + default + end + end + + def date=( time ) + if time + store 'Date', time2str(time) + else + @header.delete 'date' + end + time + end + + def strftime( fmt, default = nil ) + if t = date + t.strftime(fmt) + else + default + end + end + + + # + # destination + # + + def to_addrs( default = nil ) + if h = @header['to'] + h.addrs + else + default + end + end + + def cc_addrs( default = nil ) + if h = @header['cc'] + h.addrs + else + default + end + end + + def bcc_addrs( default = nil ) + if h = @header['bcc'] + h.addrs + else + default + end + end + + def to_addrs=( arg ) + set_addrfield 'to', arg + end + + def cc_addrs=( arg ) + set_addrfield 'cc', arg + end + + def bcc_addrs=( arg ) + set_addrfield 'bcc', arg + end + + def to( default = nil ) + addrs2specs(to_addrs(nil)) || default + end + + def cc( default = nil ) + addrs2specs(cc_addrs(nil)) || default + end + + def bcc( default = nil ) + addrs2specs(bcc_addrs(nil)) || default + end + + def to=( *strs ) + set_string_array_attr 'To', strs + end + + def cc=( *strs ) + set_string_array_attr 'Cc', strs + end + + def bcc=( *strs ) + set_string_array_attr 'Bcc', strs + end + + + # + # originator + # + + def from_addrs( default = nil ) + if h = @header['from'] + h.addrs + else + default + end + end + + def from_addrs=( arg ) + set_addrfield 'from', arg + end + + def from( default = nil ) + addrs2specs(from_addrs(nil)) || default + end + + def from=( *strs ) + set_string_array_attr 'From', strs + end + + def friendly_from( default = nil ) + h = @header['from'] + a, = h.addrs + return default unless a + return a.phrase if a.phrase + return h.comments.join(' ') unless h.comments.empty? + a.spec + end + + + def reply_to_addrs( default = nil ) + if h = @header['reply-to'] + h.addrs + else + default + end + end + + def reply_to_addrs=( arg ) + set_addrfield 'reply-to', arg + end + + def reply_to( default = nil ) + addrs2specs(reply_to_addrs(nil)) || default + end + + def reply_to=( *strs ) + set_string_array_attr 'Reply-To', strs + end + + + def sender_addr( default = nil ) + f = @header['sender'] or return default + f.addr or return default + end + + def sender_addr=( addr ) + if addr + h = HeaderField.internal_new('sender', @config) + h.addr = addr + @header['sender'] = h + else + @header.delete 'sender' + end + addr + end + + def sender( default ) + f = @header['sender'] or return default + a = f.addr or return default + a.spec + end + + def sender=( str ) + set_string_attr 'Sender', str + end + + + # + # subject + # + + def subject( default = nil ) + if h = @header['subject'] + h.body + else + default + end + end + alias quoted_subject subject + + def subject=( str ) + set_string_attr 'Subject', str + end + + + # + # identity & threading + # + + def message_id( default = nil ) + if h = @header['message-id'] + h.id || default + else + default + end + end + + def message_id=( str ) + set_string_attr 'Message-Id', str + end + + def in_reply_to( default = nil ) + if h = @header['in-reply-to'] + h.ids + else + default + end + end + + def in_reply_to=( *idstrs ) + set_string_array_attr 'In-Reply-To', idstrs + end + + def references( default = nil ) + if h = @header['references'] + h.refs + else + default + end + end + + def references=( *strs ) + set_string_array_attr 'References', strs + end + + + # + # MIME headers + # + + def mime_version( default = nil ) + if h = @header['mime-version'] + h.version || default + else + default + end + end + + def mime_version=( m, opt = nil ) + if opt + if h = @header['mime-version'] + h.major = m + h.minor = opt + else + store 'Mime-Version', "#{m}.#{opt}" + end + else + store 'Mime-Version', m + end + m + end + + + def content_type( default = nil ) + if h = @header['content-type'] + h.content_type || default + else + default + end + end + + def main_type( default = nil ) + if h = @header['content-type'] + h.main_type || default + else + default + end + end + + def sub_type( default = nil ) + if h = @header['content-type'] + h.sub_type || default + else + default + end + end + + def set_content_type( str, sub = nil, param = nil ) + if sub + main, sub = str, sub + else + main, sub = str.split(%r, 2) + raise ArgumentError, "sub type missing: #{str.inspect}" unless sub + end + if h = @header['content-type'] + h.main_type = main + h.sub_type = sub + h.params.clear + else + store 'Content-Type', "#{main}/#{sub}" + end + @header['content-type'].params.replace param if param + + str + end + + alias content_type= set_content_type + + def type_param( name, default = nil ) + if h = @header['content-type'] + h[name] || default + else + default + end + end + + def charset( default = nil ) + if h = @header['content-type'] + h['charset'] or default + else + default + end + end + + def charset=( str ) + if str + if h = @header[ 'content-type' ] + h['charset'] = str + else + store 'Content-Type', "text/plain; charset=#{str}" + end + end + str + end + + + def transfer_encoding( default = nil ) + if h = @header['content-transfer-encoding'] + h.encoding || default + else + default + end + end + + def transfer_encoding=( str ) + set_string_attr 'Content-Transfer-Encoding', str + end + + alias encoding transfer_encoding + alias encoding= transfer_encoding= + alias content_transfer_encoding transfer_encoding + alias content_transfer_encoding= transfer_encoding= + + + def disposition( default = nil ) + if h = @header['content-disposition'] + h.disposition || default + else + default + end + end + + alias content_disposition disposition + + def set_disposition( str, params = nil ) + if h = @header['content-disposition'] + h.disposition = str + h.params.clear + else + store('Content-Disposition', str) + h = @header['content-disposition'] + end + h.params.replace params if params + end + + alias disposition= set_disposition + alias set_content_disposition set_disposition + alias content_disposition= set_disposition + + def disposition_param( name, default = nil ) + if h = @header['content-disposition'] + h[name] || default + else + default + end + end + + ### + ### utils + ### + + def create_reply + mail = TMail::Mail.parse('') + mail.subject = 'Re: ' + subject('').sub(/\A(?:\[[^\]]+\])?(?:\s*Re:)*\s*/i, '') + mail.to_addrs = reply_addresses([]) + mail.in_reply_to = [message_id(nil)].compact + mail.references = references([]) + [message_id(nil)].compact + mail.mime_version = '1.0' + mail + end + + + def base64_encode + store 'Content-Transfer-Encoding', 'Base64' + self.body = Base64.folding_encode(self.body) + end + + def base64_decode + if /base64/i === self.transfer_encoding('') + store 'Content-Transfer-Encoding', '8bit' + self.body = Base64.decode(self.body, @config.strict_base64decode?) + end + end + + + def destinations( default = nil ) + ret = [] + %w( to cc bcc ).each do |nm| + if h = @header[nm] + h.addrs.each {|i| ret.push i.address } + end + end + ret.empty? ? default : ret + end + + def each_destination( &block ) + destinations([]).each do |i| + if Address === i + yield i + else + i.each(&block) + end + end + end + + alias each_dest each_destination + + + def reply_addresses( default = nil ) + reply_to_addrs(nil) or from_addrs(nil) or default + end + + def error_reply_addresses( default = nil ) + if s = sender(nil) + [s] + else + from_addrs(default) + end + end + + + def multipart? + main_type('').downcase == 'multipart' + end + + end # class Mail + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/header.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/header.rb new file mode 100755 index 00000000..be97803d --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/header.rb @@ -0,0 +1,914 @@ +# +# header.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/encode' +require 'tmail/address' +require 'tmail/parser' +require 'tmail/config' +require 'tmail/utils' + + +module TMail + + class HeaderField + + include TextUtils + + class << self + + alias newobj new + + def new( name, body, conf = DEFAULT_CONFIG ) + klass = FNAME_TO_CLASS[name.downcase] || UnstructuredHeader + klass.newobj body, conf + end + + def new_from_port( port, name, conf = DEFAULT_CONFIG ) + re = Regep.new('\A(' + Regexp.quote(name) + '):', 'i') + str = nil + port.ropen {|f| + f.each do |line| + if m = re.match(line) then str = m.post_match.strip + elsif str and /\A[\t ]/ === line then str << ' ' << line.strip + elsif /\A-*\s*\z/ === line then break + elsif str then break + end + end + } + new(name, str, Config.to_config(conf)) + end + + def internal_new( name, conf ) + FNAME_TO_CLASS[name].newobj('', conf, true) + end + + end # class << self + + def initialize( body, conf, intern = false ) + @body = body + @config = conf + + @illegal = false + @parsed = false + if intern + @parsed = true + parse_init + end + end + + def inspect + "#<#{self.class} #{@body.inspect}>" + end + + def illegal? + @illegal + end + + def empty? + ensure_parsed + return true if @illegal + isempty? + end + + private + + def ensure_parsed + return if @parsed + @parsed = true + parse + end + + # defabstract parse + # end + + def clear_parse_status + @parsed = false + @illegal = false + end + + public + + def body + ensure_parsed + v = Decoder.new(s = '') + do_accept v + v.terminate + s + end + + def body=( str ) + @body = str + clear_parse_status + end + + include StrategyInterface + + def accept( strategy, dummy1 = nil, dummy2 = nil ) + ensure_parsed + do_accept strategy + strategy.terminate + end + + # abstract do_accept + + end + + + class UnstructuredHeader < HeaderField + + def body + ensure_parsed + @body + end + + def body=( arg ) + ensure_parsed + @body = arg + end + + private + + def parse_init + end + + def parse + @body = Decoder.decode(@body.gsub(/\n|\r\n|\r/, '')) + end + + def isempty? + not @body + end + + def do_accept( strategy ) + strategy.text @body + end + + end + + + class StructuredHeader < HeaderField + + def comments + ensure_parsed + @comments + end + + private + + def parse + save = nil + + begin + parse_init + do_parse + rescue SyntaxError + if not save and mime_encoded? @body + save = @body + @body = Decoder.decode(save) + retry + elsif save + @body = save + end + + @illegal = true + raise if @config.strict_parse? + end + end + + def parse_init + @comments = [] + init + end + + def do_parse + obj = Parser.parse(self.class::PARSE_TYPE, @body, @comments) + set obj if obj + end + + end + + + class DateTimeHeader < StructuredHeader + + PARSE_TYPE = :DATETIME + + def date + ensure_parsed + @date + end + + def date=( arg ) + ensure_parsed + @date = arg + end + + private + + def init + @date = nil + end + + def set( t ) + @date = t + end + + def isempty? + not @date + end + + def do_accept( strategy ) + strategy.meta time2str(@date) + end + + end + + + class AddressHeader < StructuredHeader + + PARSE_TYPE = :MADDRESS + + def addrs + ensure_parsed + @addrs + end + + private + + def init + @addrs = [] + end + + def set( a ) + @addrs = a + end + + def isempty? + @addrs.empty? + end + + def do_accept( strategy ) + first = true + @addrs.each do |a| + if first + first = false + else + strategy.meta ',' + strategy.space + end + a.accept strategy + end + + @comments.each do |c| + strategy.space + strategy.meta '(' + strategy.text c + strategy.meta ')' + end + end + + end + + + class ReturnPathHeader < AddressHeader + + PARSE_TYPE = :RETPATH + + def addr + addrs()[0] + end + + def spec + a = addr() or return nil + a.spec + end + + def routes + a = addr() or return nil + a.routes + end + + private + + def do_accept( strategy ) + a = addr() + + strategy.meta '<' + unless a.routes.empty? + strategy.meta a.routes.map {|i| '@' + i }.join(',') + strategy.meta ':' + end + spec = a.spec + strategy.meta spec if spec + strategy.meta '>' + end + + end + + + class SingleAddressHeader < AddressHeader + + def addr + addrs()[0] + end + + private + + def do_accept( strategy ) + a = addr() + a.accept strategy + @comments.each do |c| + strategy.space + strategy.meta '(' + strategy.text c + strategy.meta ')' + end + end + + end + + + class MessageIdHeader < StructuredHeader + + def id + ensure_parsed + @id + end + + def id=( arg ) + ensure_parsed + @id = arg + end + + private + + def init + @id = nil + end + + def isempty? + not @id + end + + def do_parse + @id = @body.slice(MESSAGE_ID) or + raise SyntaxError, "wrong Message-ID format: #{@body}" + end + + def do_accept( strategy ) + strategy.meta @id + end + + end + + + class ReferencesHeader < StructuredHeader + + def refs + ensure_parsed + @refs + end + + def each_id + self.refs.each do |i| + yield i if MESSAGE_ID === i + end + end + + def ids + ensure_parsed + @ids + end + + def each_phrase + self.refs.each do |i| + yield i unless MESSAGE_ID === i + end + end + + def phrases + ret = [] + each_phrase {|i| ret.push i } + ret + end + + private + + def init + @refs = [] + @ids = [] + end + + def isempty? + @ids.empty? + end + + def do_parse + str = @body + while m = MESSAGE_ID.match(str) + pre = m.pre_match.strip + @refs.push pre unless pre.empty? + @refs.push s = m[0] + @ids.push s + str = m.post_match + end + str = str.strip + @refs.push str unless str.empty? + end + + def do_accept( strategy ) + first = true + @ids.each do |i| + if first + first = false + else + strategy.space + end + strategy.meta i + end + end + + end + + + class ReceivedHeader < StructuredHeader + + PARSE_TYPE = :RECEIVED + + def from + ensure_parsed + @from + end + + def from=( arg ) + ensure_parsed + @from = arg + end + + def by + ensure_parsed + @by + end + + def by=( arg ) + ensure_parsed + @by = arg + end + + def via + ensure_parsed + @via + end + + def via=( arg ) + ensure_parsed + @via = arg + end + + def with + ensure_parsed + @with + end + + def id + ensure_parsed + @id + end + + def id=( arg ) + ensure_parsed + @id = arg + end + + def _for + ensure_parsed + @_for + end + + def _for=( arg ) + ensure_parsed + @_for = arg + end + + def date + ensure_parsed + @date + end + + def date=( arg ) + ensure_parsed + @date = arg + end + + private + + def init + @from = @by = @via = @with = @id = @_for = nil + @with = [] + @date = nil + end + + def set( args ) + @from, @by, @via, @with, @id, @_for, @date = *args + end + + def isempty? + @with.empty? and not (@from or @by or @via or @id or @_for or @date) + end + + def do_accept( strategy ) + list = [] + list.push 'from ' + @from if @from + list.push 'by ' + @by if @by + list.push 'via ' + @via if @via + @with.each do |i| + list.push 'with ' + i + end + list.push 'id ' + @id if @id + list.push 'for <' + @_for + '>' if @_for + + first = true + list.each do |i| + strategy.space unless first + strategy.meta i + first = false + end + if @date + strategy.meta ';' + strategy.space + strategy.meta time2str(@date) + end + end + + end + + + class KeywordsHeader < StructuredHeader + + PARSE_TYPE = :KEYWORDS + + def keys + ensure_parsed + @keys + end + + private + + def init + @keys = [] + end + + def set( a ) + @keys = a + end + + def isempty? + @keys.empty? + end + + def do_accept( strategy ) + first = true + @keys.each do |i| + if first + first = false + else + strategy.meta ',' + end + strategy.meta i + end + end + + end + + + class EncryptedHeader < StructuredHeader + + PARSE_TYPE = :ENCRYPTED + + def encrypter + ensure_parsed + @encrypter + end + + def encrypter=( arg ) + ensure_parsed + @encrypter = arg + end + + def keyword + ensure_parsed + @keyword + end + + def keyword=( arg ) + ensure_parsed + @keyword = arg + end + + private + + def init + @encrypter = nil + @keyword = nil + end + + def set( args ) + @encrypter, @keyword = args + end + + def isempty? + not (@encrypter or @keyword) + end + + def do_accept( strategy ) + if @key + strategy.meta @encrypter + ',' + strategy.space + strategy.meta @keyword + else + strategy.meta @encrypter + end + end + + end + + + class MimeVersionHeader < StructuredHeader + + PARSE_TYPE = :MIMEVERSION + + def major + ensure_parsed + @major + end + + def major=( arg ) + ensure_parsed + @major = arg + end + + def minor + ensure_parsed + @minor + end + + def minor=( arg ) + ensure_parsed + @minor = arg + end + + def version + sprintf('%d.%d', major, minor) + end + + private + + def init + @major = nil + @minor = nil + end + + def set( args ) + @major, @minor = *args + end + + def isempty? + not (@major or @minor) + end + + def do_accept( strategy ) + strategy.meta sprintf('%d.%d', @major, @minor) + end + + end + + + class ContentTypeHeader < StructuredHeader + + PARSE_TYPE = :CTYPE + + def main_type + ensure_parsed + @main + end + + def main_type=( arg ) + ensure_parsed + @main = arg.downcase + end + + def sub_type + ensure_parsed + @sub + end + + def sub_type=( arg ) + ensure_parsed + @sub = arg.downcase + end + + def content_type + ensure_parsed + @sub ? sprintf('%s/%s', @main, @sub) : @main + end + + def params + ensure_parsed + @params + end + + def []( key ) + ensure_parsed + @params and @params[key] + end + + def []=( key, val ) + ensure_parsed + (@params ||= {})[key] = val + end + + private + + def init + @main = @sub = @params = nil + end + + def set( args ) + @main, @sub, @params = *args + end + + def isempty? + not (@main or @sub) + end + + def do_accept( strategy ) + if @sub + strategy.meta sprintf('%s/%s', @main, @sub) + else + strategy.meta @main + end + @params.each do |k,v| + if v + strategy.meta ';' + strategy.space + strategy.kv_pair k, v + end + end + end + + end + + + class ContentTransferEncodingHeader < StructuredHeader + + PARSE_TYPE = :CENCODING + + def encoding + ensure_parsed + @encoding + end + + def encoding=( arg ) + ensure_parsed + @encoding = arg + end + + private + + def init + @encoding = nil + end + + def set( s ) + @encoding = s + end + + def isempty? + not @encoding + end + + def do_accept( strategy ) + strategy.meta @encoding.capitalize + end + + end + + + class ContentDispositionHeader < StructuredHeader + + PARSE_TYPE = :CDISPOSITION + + def disposition + ensure_parsed + @disposition + end + + def disposition=( str ) + ensure_parsed + @disposition = str.downcase + end + + def params + ensure_parsed + @params + end + + def []( key ) + ensure_parsed + @params and @params[key] + end + + def []=( key, val ) + ensure_parsed + (@params ||= {})[key] = val + end + + private + + def init + @disposition = @params = nil + end + + def set( args ) + @disposition, @params = *args + end + + def isempty? + not @disposition and (not @params or @params.empty?) + end + + def do_accept( strategy ) + strategy.meta @disposition + @params.each do |k,v| + strategy.meta ';' + strategy.space + strategy.kv_pair k, v + end + end + + end + + + class HeaderField # redefine + + FNAME_TO_CLASS = { + 'date' => DateTimeHeader, + 'resent-date' => DateTimeHeader, + 'to' => AddressHeader, + 'cc' => AddressHeader, + 'bcc' => AddressHeader, + 'from' => AddressHeader, + 'reply-to' => AddressHeader, + 'resent-to' => AddressHeader, + 'resent-cc' => AddressHeader, + 'resent-bcc' => AddressHeader, + 'resent-from' => AddressHeader, + 'resent-reply-to' => AddressHeader, + 'sender' => SingleAddressHeader, + 'resent-sender' => SingleAddressHeader, + 'return-path' => ReturnPathHeader, + 'message-id' => MessageIdHeader, + 'resent-message-id' => MessageIdHeader, + 'in-reply-to' => ReferencesHeader, + 'received' => ReceivedHeader, + 'references' => ReferencesHeader, + 'keywords' => KeywordsHeader, + 'encrypted' => EncryptedHeader, + 'mime-version' => MimeVersionHeader, + 'content-type' => ContentTypeHeader, + 'content-transfer-encoding' => ContentTransferEncodingHeader, + 'content-disposition' => ContentDispositionHeader, + 'content-id' => MessageIdHeader, + 'subject' => UnstructuredHeader, + 'comments' => UnstructuredHeader, + 'content-description' => UnstructuredHeader + } + + end + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/info.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/info.rb new file mode 100755 index 00000000..5c115d58 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/info.rb @@ -0,0 +1,35 @@ +# +# info.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +module TMail + + Version = '0.10.7' + Copyright = 'Copyright (c) 1998-2002 Minero Aoki' + +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/loader.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/loader.rb new file mode 100755 index 00000000..79073154 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/loader.rb @@ -0,0 +1 @@ +require 'tmail/mailbox' diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mail.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mail.rb new file mode 100755 index 00000000..e11fa0f0 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mail.rb @@ -0,0 +1,447 @@ +# +# mail.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/facade' +require 'tmail/encode' +require 'tmail/header' +require 'tmail/port' +require 'tmail/config' +require 'tmail/utils' +require 'tmail/attachments' +require 'tmail/quoting' +require 'socket' + + +module TMail + + class Mail + + class << self + def load( fname ) + new(FilePort.new(fname)) + end + + alias load_from load + alias loadfrom load + + def parse( str ) + new(StringPort.new(str)) + end + end + + def initialize( port = nil, conf = DEFAULT_CONFIG ) + @port = port || StringPort.new + @config = Config.to_config(conf) + + @header = {} + @body_port = nil + @body_parsed = false + @epilogue = '' + @parts = [] + + @port.ropen {|f| + parse_header f + parse_body f unless @port.reproducible? + } + end + + attr_reader :port + + def inspect + "\#<#{self.class} port=#{@port.inspect} bodyport=#{@body_port.inspect}>" + end + + # + # to_s interfaces + # + + public + + include StrategyInterface + + def write_back( eol = "\n", charset = 'e' ) + parse_body + @port.wopen {|stream| encoded eol, charset, stream } + end + + def accept( strategy ) + with_multipart_encoding(strategy) { + ordered_each do |name, field| + next if field.empty? + strategy.header_name canonical(name) + field.accept strategy + strategy.puts + end + strategy.puts + body_port().ropen {|r| + strategy.write r.read + } + } + end + + private + + def canonical( name ) + name.split(/-/).map {|s| s.capitalize }.join('-') + end + + def with_multipart_encoding( strategy ) + if parts().empty? # DO NOT USE @parts + yield + + else + bound = ::TMail.new_boundary + if @header.key? 'content-type' + @header['content-type'].params['boundary'] = bound + else + store 'Content-Type', % + end + + yield + + parts().each do |tm| + strategy.puts + strategy.puts '--' + bound + tm.accept strategy + end + strategy.puts + strategy.puts '--' + bound + '--' + strategy.write epilogue() + end + end + + ### + ### header + ### + + public + + ALLOW_MULTIPLE = { + 'received' => true, + 'resent-date' => true, + 'resent-from' => true, + 'resent-sender' => true, + 'resent-to' => true, + 'resent-cc' => true, + 'resent-bcc' => true, + 'resent-message-id' => true, + 'comments' => true, + 'keywords' => true + } + USE_ARRAY = ALLOW_MULTIPLE + + def header + @header.dup + end + + def []( key ) + @header[key.downcase] + end + + def sub_header(key, param) + (hdr = self[key]) ? hdr[param] : nil + end + + alias fetch [] + + def []=( key, val ) + dkey = key.downcase + + if val.nil? + @header.delete dkey + return nil + end + + case val + when String + header = new_hf(key, val) + when HeaderField + ; + when Array + ALLOW_MULTIPLE.include? dkey or + raise ArgumentError, "#{key}: Header must not be multiple" + @header[dkey] = val + return val + else + header = new_hf(key, val.to_s) + end + if ALLOW_MULTIPLE.include? dkey + (@header[dkey] ||= []).push header + else + @header[dkey] = header + end + + val + end + + alias store []= + + def each_header + @header.each do |key, val| + [val].flatten.each {|v| yield key, v } + end + end + + alias each_pair each_header + + def each_header_name( &block ) + @header.each_key(&block) + end + + alias each_key each_header_name + + def each_field( &block ) + @header.values.flatten.each(&block) + end + + alias each_value each_field + + FIELD_ORDER = %w( + return-path received + resent-date resent-from resent-sender resent-to + resent-cc resent-bcc resent-message-id + date from sender reply-to to cc bcc + message-id in-reply-to references + subject comments keywords + mime-version content-type content-transfer-encoding + content-disposition content-description + ) + + def ordered_each + list = @header.keys + FIELD_ORDER.each do |name| + if list.delete(name) + [@header[name]].flatten.each {|v| yield name, v } + end + end + list.each do |name| + [@header[name]].flatten.each {|v| yield name, v } + end + end + + def clear + @header.clear + end + + def delete( key ) + @header.delete key.downcase + end + + def delete_if + @header.delete_if do |key,val| + if Array === val + val.delete_if {|v| yield key, v } + val.empty? + else + yield key, val + end + end + end + + def keys + @header.keys + end + + def key?( key ) + @header.key? key.downcase + end + + def values_at( *args ) + args.map {|k| @header[k.downcase] }.flatten + end + + alias indexes values_at + alias indices values_at + + private + + def parse_header( f ) + name = field = nil + unixfrom = nil + + while line = f.gets + case line + when /\A[ \t]/ # continue from prev line + raise SyntaxError, 'mail is began by space' unless field + field << ' ' << line.strip + + when /\A([^\: \t]+):\s*/ # new header line + add_hf name, field if field + name = $1 + field = $' #.strip + + when /\A\-*\s*\z/ # end of header + add_hf name, field if field + name = field = nil + break + + when /\AFrom (\S+)/ + unixfrom = $1 + + when /^charset=.*/ + + else + raise SyntaxError, "wrong mail header: '#{line.inspect}'" + end + end + add_hf name, field if name + + if unixfrom + add_hf 'Return-Path', "<#{unixfrom}>" unless @header['return-path'] + end + end + + def add_hf( name, field ) + key = name.downcase + field = new_hf(name, field) + + if ALLOW_MULTIPLE.include? key + (@header[key] ||= []).push field + else + @header[key] = field + end + end + + def new_hf( name, field ) + HeaderField.new(name, field, @config) + end + + ### + ### body + ### + + public + + def body_port + parse_body + @body_port + end + + def each( &block ) + body_port().ropen {|f| f.each(&block) } + end + + def quoted_body + parse_body + @body_port.ropen {|f| + return f.read + } + end + + def body=( str ) + parse_body + @body_port.wopen {|f| f.write str } + str + end + + alias preamble body + alias preamble= body= + + def epilogue + parse_body + @epilogue.dup + end + + def epilogue=( str ) + parse_body + @epilogue = str + str + end + + def parts + parse_body + @parts + end + + def each_part( &block ) + parts().each(&block) + end + + private + + def parse_body( f = nil ) + return if @body_parsed + if f + parse_body_0 f + else + @port.ropen {|f| + skip_header f + parse_body_0 f + } + end + @body_parsed = true + end + + def skip_header( f ) + while line = f.gets + return if /\A[\r\n]*\z/ === line + end + end + + def parse_body_0( f ) + if multipart? + read_multipart f + else + @body_port = @config.new_body_port(self) + @body_port.wopen {|w| + w.write f.read + } + end + end + + def read_multipart( src ) + bound = @header['content-type'].params['boundary'] + is_sep = /\A--#{Regexp.quote bound}(?:--)?[ \t]*(?:\n|\r\n|\r)/ + lastbound = "--#{bound}--" + + ports = [ @config.new_preamble_port(self) ] + begin + f = ports.last.wopen + while line = src.gets + if is_sep === line + f.close + break if line.strip == lastbound + ports.push @config.new_part_port(self) + f = ports.last.wopen + else + f << line + end + end + @epilogue = (src.read || '') + ensure + f.close if f and not f.closed? + end + + @body_port = ports.shift + @parts = ports.map {|p| self.class.new(p, @config) } + end + + end # class Mail + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mailbox.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mailbox.rb new file mode 100755 index 00000000..d85915ed --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mailbox.rb @@ -0,0 +1,433 @@ +# +# mailbox.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/port' +require 'socket' +require 'mutex_m' + + +unless [].respond_to?(:sort_by) +module Enumerable#:nodoc: + def sort_by + map {|i| [yield(i), i] }.sort {|a,b| a.first <=> b.first }.map {|i| i[1] } + end +end +end + + +module TMail + + class MhMailbox + + PORT_CLASS = MhPort + + def initialize( dir ) + edir = File.expand_path(dir) + raise ArgumentError, "not directory: #{dir}"\ + unless FileTest.directory? edir + @dirname = edir + @last_file = nil + @last_atime = nil + end + + def directory + @dirname + end + + alias dirname directory + + attr_accessor :last_atime + + def inspect + "#<#{self.class} #{@dirname}>" + end + + def close + end + + def new_port + PORT_CLASS.new(next_file_name()) + end + + def each_port + mail_files().each do |path| + yield PORT_CLASS.new(path) + end + @last_atime = Time.now + end + + alias each each_port + + def reverse_each_port + mail_files().reverse_each do |path| + yield PORT_CLASS.new(path) + end + @last_atime = Time.now + end + + alias reverse_each reverse_each_port + + # old #each_mail returns Port + #def each_mail + # each_port do |port| + # yield Mail.new(port) + # end + #end + + def each_new_port( mtime = nil, &block ) + mtime ||= @last_atime + return each_port(&block) unless mtime + return unless File.mtime(@dirname) >= mtime + + mail_files().each do |path| + yield PORT_CLASS.new(path) if File.mtime(path) > mtime + end + @last_atime = Time.now + end + + private + + def mail_files + Dir.entries(@dirname)\ + .select {|s| /\A\d+\z/ === s }\ + .map {|s| s.to_i }\ + .sort\ + .map {|i| "#{@dirname}/#{i}" }\ + .select {|path| FileTest.file? path } + end + + def next_file_name + unless n = @last_file + n = 0 + Dir.entries(@dirname)\ + .select {|s| /\A\d+\z/ === s }\ + .map {|s| s.to_i }.sort\ + .each do |i| + next unless FileTest.file? "#{@dirname}/#{i}" + n = i + end + end + begin + n += 1 + end while FileTest.exist? "#{@dirname}/#{n}" + @last_file = n + + "#{@dirname}/#{n}" + end + + end # MhMailbox + + MhLoader = MhMailbox + + + class UNIXMbox + + def UNIXMbox.lock( fname ) + begin + f = File.open(fname) + f.flock File::LOCK_EX + yield f + ensure + f.flock File::LOCK_UN + f.close if f and not f.closed? + end + end + + class << self + alias newobj new + end + + def UNIXMbox.new( fname, tmpdir = nil, readonly = false ) + tmpdir = ENV['TEMP'] || ENV['TMP'] || '/tmp' + newobj(fname, "#{tmpdir}/ruby_tmail_#{$$}_#{rand()}", readonly, false) + end + + def UNIXMbox.static_new( fname, dir, readonly = false ) + newobj(fname, dir, readonly, true) + end + + def initialize( fname, mhdir, readonly, static ) + @filename = fname + @readonly = readonly + @closed = false + + Dir.mkdir mhdir + @real = MhMailbox.new(mhdir) + @finalizer = UNIXMbox.mkfinal(@real, @filename, !@readonly, !static) + ObjectSpace.define_finalizer self, @finalizer + end + + def UNIXMbox.mkfinal( mh, mboxfile, writeback_p, cleanup_p ) + lambda { + if writeback_p + lock(mboxfile) {|f| + mh.each_port do |port| + f.puts create_from_line(port) + port.ropen {|r| + f.puts r.read + } + end + } + end + if cleanup_p + Dir.foreach(mh.dirname) do |fname| + next if /\A\.\.?\z/ === fname + File.unlink "#{mh.dirname}/#{fname}" + end + Dir.rmdir mh.dirname + end + } + end + + # make _From line + def UNIXMbox.create_from_line( port ) + sprintf 'From %s %s', + fromaddr(), TextUtils.time2str(File.mtime(port.filename)) + end + + def UNIXMbox.fromaddr + h = HeaderField.new_from_port(port, 'Return-Path') || + HeaderField.new_from_port(port, 'From') or return 'nobody' + a = h.addrs[0] or return 'nobody' + a.spec + end + private_class_method :fromaddr + + def close + return if @closed + + ObjectSpace.undefine_finalizer self + @finalizer.call + @finalizer = nil + @real = nil + @closed = true + @updated = nil + end + + def each_port( &block ) + close_check + update + @real.each_port(&block) + end + + alias each each_port + + def reverse_each_port( &block ) + close_check + update + @real.reverse_each_port(&block) + end + + alias reverse_each reverse_each_port + + # old #each_mail returns Port + #def each_mail( &block ) + # each_port do |port| + # yield Mail.new(port) + # end + #end + + def each_new_port( mtime = nil ) + close_check + update + @real.each_new_port(mtime) {|p| yield p } + end + + def new_port + close_check + @real.new_port + end + + private + + def close_check + @closed and raise ArgumentError, 'accessing already closed mbox' + end + + def update + return if FileTest.zero?(@filename) + return if @updated and File.mtime(@filename) < @updated + w = nil + port = nil + time = nil + UNIXMbox.lock(@filename) {|f| + begin + f.each do |line| + if /\AFrom / === line + w.close if w + File.utime time, time, port.filename if time + + port = @real.new_port + w = port.wopen + time = fromline2time(line) + else + w.print line if w + end + end + ensure + if w and not w.closed? + w.close + File.utime time, time, port.filename if time + end + end + f.truncate(0) unless @readonly + @updated = Time.now + } + end + + def fromline2time( line ) + m = /\AFrom \S+ \w+ (\w+) (\d+) (\d+):(\d+):(\d+) (\d+)/.match(line) \ + or return nil + Time.local(m[6].to_i, m[1], m[2].to_i, m[3].to_i, m[4].to_i, m[5].to_i) + end + + end # UNIXMbox + + MboxLoader = UNIXMbox + + + class Maildir + + extend Mutex_m + + PORT_CLASS = MaildirPort + + @seq = 0 + def Maildir.unique_number + synchronize { + @seq += 1 + return @seq + } + end + + def initialize( dir = nil ) + @dirname = dir || ENV['MAILDIR'] + raise ArgumentError, "not directory: #{@dirname}"\ + unless FileTest.directory? @dirname + @new = "#{@dirname}/new" + @tmp = "#{@dirname}/tmp" + @cur = "#{@dirname}/cur" + end + + def directory + @dirname + end + + def inspect + "#<#{self.class} #{@dirname}>" + end + + def close + end + + def each_port + mail_files(@cur).each do |path| + yield PORT_CLASS.new(path) + end + end + + alias each each_port + + def reverse_each_port + mail_files(@cur).reverse_each do |path| + yield PORT_CLASS.new(path) + end + end + + alias reverse_each reverse_each_port + + def new_port + fname = nil + tmpfname = nil + newfname = nil + + begin + fname = "#{Time.now.to_i}.#{$$}_#{Maildir.unique_number}.#{Socket.gethostname}" + + tmpfname = "#{@tmp}/#{fname}" + newfname = "#{@new}/#{fname}" + end while FileTest.exist? tmpfname + + if block_given? + File.open(tmpfname, 'w') {|f| yield f } + File.rename tmpfname, newfname + PORT_CLASS.new(newfname) + else + File.open(tmpfname, 'w') {|f| f.write "\n\n" } + PORT_CLASS.new(tmpfname) + end + end + + def each_new_port + mail_files(@new).each do |path| + dest = @cur + '/' + File.basename(path) + File.rename path, dest + yield PORT_CLASS.new(dest) + end + + check_tmp + end + + TOO_OLD = 60 * 60 * 36 # 36 hour + + def check_tmp + old = Time.now.to_i - TOO_OLD + + each_filename(@tmp) do |full, fname| + if FileTest.file? full and + File.stat(full).mtime.to_i < old + File.unlink full + end + end + end + + private + + def mail_files( dir ) + Dir.entries(dir)\ + .select {|s| s[0] != ?. }\ + .sort_by {|s| s.slice(/\A\d+/).to_i }\ + .map {|s| "#{dir}/#{s}" }\ + .select {|path| FileTest.file? path } + end + + def each_filename( dir ) + Dir.foreach(dir) do |fname| + path = "#{dir}/#{fname}" + if fname[0] != ?. and FileTest.file? path + yield path, fname + end + end + end + + end # Maildir + + MaildirLoader = Maildir + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mbox.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mbox.rb new file mode 100755 index 00000000..79073154 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mbox.rb @@ -0,0 +1 @@ +require 'tmail/mailbox' diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/net.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/net.rb new file mode 100755 index 00000000..f96cf64c --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/net.rb @@ -0,0 +1,280 @@ +# +# net.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'nkf' + + +module TMail + + class Mail + + def send_to( smtp ) + do_send_to(smtp) do + ready_to_send + end + end + + def send_text_to( smtp ) + do_send_to(smtp) do + ready_to_send + mime_encode + end + end + + def do_send_to( smtp ) + from = from_address or raise ArgumentError, 'no from address' + (dests = destinations).empty? and raise ArgumentError, 'no receipient' + yield + send_to_0 smtp, from, dests + end + private :do_send_to + + def send_to_0( smtp, from, to ) + smtp.ready(from, to) do |f| + encoded "\r\n", 'j', f, '' + end + end + + def ready_to_send + delete_no_send_fields + add_message_id + add_date + end + + NOSEND_FIELDS = %w( + received + bcc + ) + + def delete_no_send_fields + NOSEND_FIELDS.each do |nm| + delete nm + end + delete_if {|n,v| v.empty? } + end + + def add_message_id( fqdn = nil ) + self.message_id = ::TMail::new_message_id(fqdn) + end + + def add_date + self.date = Time.now + end + + def mime_encode + if parts.empty? + mime_encode_singlepart + else + mime_encode_multipart true + end + end + + def mime_encode_singlepart + self.mime_version = '1.0' + b = body + if NKF.guess(b) != NKF::BINARY + mime_encode_text b + else + mime_encode_binary b + end + end + + def mime_encode_text( body ) + self.body = NKF.nkf('-j -m0', body) + self.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'} + self.encoding = '7bit' + end + + def mime_encode_binary( body ) + self.body = [body].pack('m') + self.set_content_type 'application', 'octet-stream' + self.encoding = 'Base64' + end + + def mime_encode_multipart( top = true ) + self.mime_version = '1.0' if top + self.set_content_type 'multipart', 'mixed' + e = encoding(nil) + if e and not /\A(?:7bit|8bit|binary)\z/i === e + raise ArgumentError, + 'using C.T.Encoding with multipart mail is not permitted' + end + end + + def create_empty_mail + self.class.new(StringPort.new(''), @config) + end + + def create_reply + setup_reply create_empty_mail() + end + + def setup_reply( m ) + if tmp = reply_addresses(nil) + m.to_addrs = tmp + end + + mid = message_id(nil) + tmp = references(nil) || [] + tmp.push mid if mid + m.in_reply_to = [mid] if mid + m.references = tmp unless tmp.empty? + m.subject = 'Re: ' + subject('').sub(/\A(?:\s*re:)+/i, '') + + m + end + + def create_forward + setup_forward create_empty_mail() + end + + def setup_forward( mail ) + m = Mail.new(StringPort.new('')) + m.body = decoded + m.set_content_type 'message', 'rfc822' + m.encoding = encoding('7bit') + mail.parts.push m + end + + end + + + class DeleteFields + + NOSEND_FIELDS = %w( + received + bcc + ) + + def initialize( nosend = nil, delempty = true ) + @no_send_fields = nosend || NOSEND_FIELDS.dup + @delete_empty_fields = delempty + end + + attr :no_send_fields + attr :delete_empty_fields, true + + def exec( mail ) + @no_send_fields.each do |nm| + delete nm + end + delete_if {|n,v| v.empty? } if @delete_empty_fields + end + + end + + + class AddMessageId + + def initialize( fqdn = nil ) + @fqdn = fqdn + end + + attr :fqdn, true + + def exec( mail ) + mail.message_id = ::TMail::new_msgid(@fqdn) + end + + end + + + class AddDate + + def exec( mail ) + mail.date = Time.now + end + + end + + + class MimeEncodeAuto + + def initialize( s = nil, m = nil ) + @singlepart_composer = s || MimeEncodeSingle.new + @multipart_composer = m || MimeEncodeMulti.new + end + + attr :singlepart_composer + attr :multipart_composer + + def exec( mail ) + if mail._builtin_multipart? + then @multipart_composer + else @singlepart_composer end.exec mail + end + + end + + + class MimeEncodeSingle + + def exec( mail ) + mail.mime_version = '1.0' + b = mail.body + if NKF.guess(b) != NKF::BINARY + on_text b + else + on_binary b + end + end + + def on_text( body ) + mail.body = NKF.nkf('-j -m0', body) + mail.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'} + mail.encoding = '7bit' + end + + def on_binary( body ) + mail.body = [body].pack('m') + mail.set_content_type 'application', 'octet-stream' + mail.encoding = 'Base64' + end + + end + + + class MimeEncodeMulti + + def exec( mail, top = true ) + mail.mime_version = '1.0' if top + mail.set_content_type 'multipart', 'mixed' + e = encoding(nil) + if e and not /\A(?:7bit|8bit|binary)\z/i === e + raise ArgumentError, + 'using C.T.Encoding with multipart mail is not permitted' + end + mail.parts.each do |m| + exec m, false if m._builtin_multipart? + end + end + + end + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/obsolete.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/obsolete.rb new file mode 100755 index 00000000..f98be747 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/obsolete.rb @@ -0,0 +1,135 @@ +# +# obsolete.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +module TMail + + # mail.rb + class Mail + alias include? key? + alias has_key? key? + + def values + ret = [] + each_field {|v| ret.push v } + ret + end + + def value?( val ) + HeaderField === val or return false + + [ @header[val.name.downcase] ].flatten.include? val + end + + alias has_value? value? + end + + + # facade.rb + class Mail + def from_addr( default = nil ) + addr, = from_addrs(nil) + addr || default + end + + def from_address( default = nil ) + if a = from_addr(nil) + a.spec + else + default + end + end + + alias from_address= from_addrs= + + def from_phrase( default = nil ) + if a = from_addr(nil) + a.phrase + else + default + end + end + + alias msgid message_id + alias msgid= message_id= + + alias each_dest each_destination + end + + + # address.rb + class Address + alias route routes + alias addr spec + + def spec=( str ) + @local, @domain = str.split(/@/,2).map {|s| s.split(/\./) } + end + + alias addr= spec= + alias address= spec= + end + + + # mbox.rb + class MhMailbox + alias new_mail new_port + alias each_mail each_port + alias each_newmail each_new_port + end + class UNIXMbox + alias new_mail new_port + alias each_mail each_port + alias each_newmail each_new_port + end + class Maildir + alias new_mail new_port + alias each_mail each_port + alias each_newmail each_new_port + end + + + # utils.rb + extend TextUtils + + class << self + alias msgid? message_id? + alias boundary new_boundary + alias msgid new_message_id + alias new_msgid new_message_id + end + + def Mail.boundary + ::TMail.new_boundary + end + + def Mail.msgid + ::TMail.new_message_id + end + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/parser.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/parser.rb new file mode 100755 index 00000000..825eca92 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/parser.rb @@ -0,0 +1,1522 @@ +# +# DO NOT MODIFY!!!! +# This file is automatically generated by racc 1.4.3 +# from racc grammer file "parser.y". +# +# +# parser.rb: generated by racc (runtime embedded) +# + +###### racc/parser.rb + +unless $".index 'racc/parser.rb' +$".push 'racc/parser.rb' + +self.class.module_eval <<'..end /home/aamine/lib/ruby/racc/parser.rb modeval..idb76f2e220d', '/home/aamine/lib/ruby/racc/parser.rb', 1 +# +# parser.rb +# +# Copyright (c) 1999-2003 Minero Aoki +# +# This program is free software. +# You can distribute/modify this program under the same terms of ruby. +# +# As a special exception, when this code is copied by Racc +# into a Racc output file, you may use that output file +# without restriction. +# +# $Id: parser.rb,v 1.1.1.1 2004/10/14 11:59:58 webster132 Exp $ +# + +unless defined? NotImplementedError + NotImplementedError = NotImplementError +end + + +module Racc + class ParseError < StandardError; end +end +unless defined?(::ParseError) + ParseError = Racc::ParseError +end + + +module Racc + + unless defined? Racc_No_Extentions + Racc_No_Extentions = false + end + + class Parser + + Racc_Runtime_Version = '1.4.3' + Racc_Runtime_Revision = '$Revision: 1.1.1.1 $'.split(/\s+/)[1] + + Racc_Runtime_Core_Version_R = '1.4.3' + Racc_Runtime_Core_Revision_R = '$Revision: 1.1.1.1 $'.split(/\s+/)[1] + begin + require 'racc/cparse' + # Racc_Runtime_Core_Version_C = (defined in extention) + Racc_Runtime_Core_Revision_C = Racc_Runtime_Core_Id_C.split(/\s+/)[2] + unless new.respond_to?(:_racc_do_parse_c, true) + raise LoadError, 'old cparse.so' + end + if Racc_No_Extentions + raise LoadError, 'selecting ruby version of racc runtime core' + end + + Racc_Main_Parsing_Routine = :_racc_do_parse_c + Racc_YY_Parse_Method = :_racc_yyparse_c + Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_C + Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_C + Racc_Runtime_Type = 'c' + rescue LoadError + Racc_Main_Parsing_Routine = :_racc_do_parse_rb + Racc_YY_Parse_Method = :_racc_yyparse_rb + Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_R + Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_R + Racc_Runtime_Type = 'ruby' + end + + def self.racc_runtime_type + Racc_Runtime_Type + end + + private + + def _racc_setup + @yydebug = false unless self.class::Racc_debug_parser + @yydebug = false unless defined? @yydebug + if @yydebug + @racc_debug_out = $stderr unless defined? @racc_debug_out + @racc_debug_out ||= $stderr + end + arg = self.class::Racc_arg + arg[13] = true if arg.size < 14 + arg + end + + def _racc_init_sysvars + @racc_state = [0] + @racc_tstack = [] + @racc_vstack = [] + + @racc_t = nil + @racc_val = nil + + @racc_read_next = true + + @racc_user_yyerror = false + @racc_error_status = 0 + end + + + ### + ### do_parse + ### + + def do_parse + __send__ Racc_Main_Parsing_Routine, _racc_setup(), false + end + + def next_token + raise NotImplementedError, "#{self.class}\#next_token is not defined" + end + + def _racc_do_parse_rb( arg, in_debug ) + action_table, action_check, action_default, action_pointer, + goto_table, goto_check, goto_default, goto_pointer, + nt_base, reduce_table, token_table, shift_n, + reduce_n, use_result, * = arg + + _racc_init_sysvars + tok = act = i = nil + nerr = 0 + + catch(:racc_end_parse) { + while true + if i = action_pointer[@racc_state[-1]] + if @racc_read_next + if @racc_t != 0 # not EOF + tok, @racc_val = next_token() + unless tok # EOF + @racc_t = 0 + else + @racc_t = (token_table[tok] or 1) # error token + end + racc_read_token(@racc_t, tok, @racc_val) if @yydebug + @racc_read_next = false + end + end + i += @racc_t + if i >= 0 and + act = action_table[i] and + action_check[i] == @racc_state[-1] + ; + else + act = action_default[@racc_state[-1]] + end + else + act = action_default[@racc_state[-1]] + end + while act = _racc_evalact(act, arg) + end + end + } + end + + + ### + ### yyparse + ### + + def yyparse( recv, mid ) + __send__ Racc_YY_Parse_Method, recv, mid, _racc_setup(), true + end + + def _racc_yyparse_rb( recv, mid, arg, c_debug ) + action_table, action_check, action_default, action_pointer, + goto_table, goto_check, goto_default, goto_pointer, + nt_base, reduce_table, token_table, shift_n, + reduce_n, use_result, * = arg + + _racc_init_sysvars + tok = nil + act = nil + i = nil + nerr = 0 + + + catch(:racc_end_parse) { + until i = action_pointer[@racc_state[-1]] + while act = _racc_evalact(action_default[@racc_state[-1]], arg) + end + end + + recv.__send__(mid) do |tok, val| +# $stderr.puts "rd: tok=#{tok}, val=#{val}" + unless tok + @racc_t = 0 + else + @racc_t = (token_table[tok] or 1) # error token + end + @racc_val = val + @racc_read_next = false + + i += @racc_t + if i >= 0 and + act = action_table[i] and + action_check[i] == @racc_state[-1] + ; +# $stderr.puts "01: act=#{act}" + else + act = action_default[@racc_state[-1]] +# $stderr.puts "02: act=#{act}" +# $stderr.puts "curstate=#{@racc_state[-1]}" + end + + while act = _racc_evalact(act, arg) + end + + while not (i = action_pointer[@racc_state[-1]]) or + not @racc_read_next or + @racc_t == 0 # $ + if i and i += @racc_t and + i >= 0 and + act = action_table[i] and + action_check[i] == @racc_state[-1] + ; +# $stderr.puts "03: act=#{act}" + else +# $stderr.puts "04: act=#{act}" + act = action_default[@racc_state[-1]] + end + + while act = _racc_evalact(act, arg) + end + end + end + } + end + + + ### + ### common + ### + + def _racc_evalact( act, arg ) +# $stderr.puts "ea: act=#{act}" + action_table, action_check, action_default, action_pointer, + goto_table, goto_check, goto_default, goto_pointer, + nt_base, reduce_table, token_table, shift_n, + reduce_n, use_result, * = arg +nerr = 0 # tmp + + if act > 0 and act < shift_n + # + # shift + # + + if @racc_error_status > 0 + @racc_error_status -= 1 unless @racc_t == 1 # error token + end + + @racc_vstack.push @racc_val + @racc_state.push act + @racc_read_next = true + + if @yydebug + @racc_tstack.push @racc_t + racc_shift @racc_t, @racc_tstack, @racc_vstack + end + + elsif act < 0 and act > -reduce_n + # + # reduce + # + + code = catch(:racc_jump) { + @racc_state.push _racc_do_reduce(arg, act) + false + } + if code + case code + when 1 # yyerror + @racc_user_yyerror = true # user_yyerror + return -reduce_n + when 2 # yyaccept + return shift_n + else + raise RuntimeError, '[Racc Bug] unknown jump code' + end + end + + elsif act == shift_n + # + # accept + # + + racc_accept if @yydebug + throw :racc_end_parse, @racc_vstack[0] + + elsif act == -reduce_n + # + # error + # + + case @racc_error_status + when 0 + unless arg[21] # user_yyerror + nerr += 1 + on_error @racc_t, @racc_val, @racc_vstack + end + when 3 + if @racc_t == 0 # is $ + throw :racc_end_parse, nil + end + @racc_read_next = true + end + @racc_user_yyerror = false + @racc_error_status = 3 + + while true + if i = action_pointer[@racc_state[-1]] + i += 1 # error token + if i >= 0 and + (act = action_table[i]) and + action_check[i] == @racc_state[-1] + break + end + end + + throw :racc_end_parse, nil if @racc_state.size < 2 + @racc_state.pop + @racc_vstack.pop + if @yydebug + @racc_tstack.pop + racc_e_pop @racc_state, @racc_tstack, @racc_vstack + end + end + + return act + + else + raise RuntimeError, "[Racc Bug] unknown action #{act.inspect}" + end + + racc_next_state(@racc_state[-1], @racc_state) if @yydebug + + nil + end + + def _racc_do_reduce( arg, act ) + action_table, action_check, action_default, action_pointer, + goto_table, goto_check, goto_default, goto_pointer, + nt_base, reduce_table, token_table, shift_n, + reduce_n, use_result, * = arg + state = @racc_state + vstack = @racc_vstack + tstack = @racc_tstack + + i = act * -3 + len = reduce_table[i] + reduce_to = reduce_table[i+1] + method_id = reduce_table[i+2] + void_array = [] + + tmp_t = tstack[-len, len] if @yydebug + tmp_v = vstack[-len, len] + tstack[-len, len] = void_array if @yydebug + vstack[-len, len] = void_array + state[-len, len] = void_array + + # tstack must be updated AFTER method call + if use_result + vstack.push __send__(method_id, tmp_v, vstack, tmp_v[0]) + else + vstack.push __send__(method_id, tmp_v, vstack) + end + tstack.push reduce_to + + racc_reduce(tmp_t, reduce_to, tstack, vstack) if @yydebug + + k1 = reduce_to - nt_base + if i = goto_pointer[k1] + i += state[-1] + if i >= 0 and (curstate = goto_table[i]) and goto_check[i] == k1 + return curstate + end + end + goto_default[k1] + end + + def on_error( t, val, vstack ) + raise ParseError, sprintf("\nparse error on value %s (%s)", + val.inspect, token_to_str(t) || '?') + end + + def yyerror + throw :racc_jump, 1 + end + + def yyaccept + throw :racc_jump, 2 + end + + def yyerrok + @racc_error_status = 0 + end + + + # for debugging output + + def racc_read_token( t, tok, val ) + @racc_debug_out.print 'read ' + @racc_debug_out.print tok.inspect, '(', racc_token2str(t), ') ' + @racc_debug_out.puts val.inspect + @racc_debug_out.puts + end + + def racc_shift( tok, tstack, vstack ) + @racc_debug_out.puts "shift #{racc_token2str tok}" + racc_print_stacks tstack, vstack + @racc_debug_out.puts + end + + def racc_reduce( toks, sim, tstack, vstack ) + out = @racc_debug_out + out.print 'reduce ' + if toks.empty? + out.print ' ' + else + toks.each {|t| out.print ' ', racc_token2str(t) } + end + out.puts " --> #{racc_token2str(sim)}" + + racc_print_stacks tstack, vstack + @racc_debug_out.puts + end + + def racc_accept + @racc_debug_out.puts 'accept' + @racc_debug_out.puts + end + + def racc_e_pop( state, tstack, vstack ) + @racc_debug_out.puts 'error recovering mode: pop token' + racc_print_states state + racc_print_stacks tstack, vstack + @racc_debug_out.puts + end + + def racc_next_state( curstate, state ) + @racc_debug_out.puts "goto #{curstate}" + racc_print_states state + @racc_debug_out.puts + end + + def racc_print_stacks( t, v ) + out = @racc_debug_out + out.print ' [' + t.each_index do |i| + out.print ' (', racc_token2str(t[i]), ' ', v[i].inspect, ')' + end + out.puts ' ]' + end + + def racc_print_states( s ) + out = @racc_debug_out + out.print ' [' + s.each {|st| out.print ' ', st } + out.puts ' ]' + end + + def racc_token2str( tok ) + self.class::Racc_token_to_s_table[tok] or + raise RuntimeError, "[Racc Bug] can't convert token #{tok} to string" + end + + def token_to_str( t ) + self.class::Racc_token_to_s_table[t] + end + + end + +end +..end /home/aamine/lib/ruby/racc/parser.rb modeval..idb76f2e220d +end # end of racc/parser.rb + + +# +# parser.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/scanner' +require 'tmail/utils' + + +module TMail + + class Parser < Racc::Parser + +module_eval <<'..end parser.y modeval..id43721faf1c', 'parser.y', 331 + + include TextUtils + + def self.parse( ident, str, cmt = nil ) + new.parse(ident, str, cmt) + end + + MAILP_DEBUG = false + + def initialize + self.debug = MAILP_DEBUG + end + + def debug=( flag ) + @yydebug = flag && Racc_debug_parser + @scanner_debug = flag + end + + def debug + @yydebug + end + + def parse( ident, str, comments = nil ) + @scanner = Scanner.new(str, ident, comments) + @scanner.debug = @scanner_debug + @first = [ident, ident] + result = yyparse(self, :parse_in) + comments.map! {|c| to_kcode(c) } if comments + result + end + + private + + def parse_in( &block ) + yield @first + @scanner.scan(&block) + end + + def on_error( t, val, vstack ) + raise SyntaxError, "parse error on token #{racc_token2str t}" + end + +..end parser.y modeval..id43721faf1c + +##### racc 1.4.3 generates ### + +racc_reduce_table = [ + 0, 0, :racc_error, + 2, 35, :_reduce_1, + 2, 35, :_reduce_2, + 2, 35, :_reduce_3, + 2, 35, :_reduce_4, + 2, 35, :_reduce_5, + 2, 35, :_reduce_6, + 2, 35, :_reduce_7, + 2, 35, :_reduce_8, + 2, 35, :_reduce_9, + 2, 35, :_reduce_10, + 2, 35, :_reduce_11, + 2, 35, :_reduce_12, + 6, 36, :_reduce_13, + 0, 48, :_reduce_none, + 2, 48, :_reduce_none, + 3, 49, :_reduce_16, + 5, 49, :_reduce_17, + 1, 50, :_reduce_18, + 7, 37, :_reduce_19, + 0, 51, :_reduce_none, + 2, 51, :_reduce_21, + 0, 52, :_reduce_none, + 2, 52, :_reduce_23, + 1, 58, :_reduce_24, + 3, 58, :_reduce_25, + 2, 58, :_reduce_26, + 0, 53, :_reduce_none, + 2, 53, :_reduce_28, + 0, 54, :_reduce_29, + 3, 54, :_reduce_30, + 0, 55, :_reduce_none, + 2, 55, :_reduce_32, + 2, 55, :_reduce_33, + 0, 56, :_reduce_none, + 2, 56, :_reduce_35, + 1, 61, :_reduce_36, + 1, 61, :_reduce_37, + 0, 57, :_reduce_none, + 2, 57, :_reduce_39, + 1, 38, :_reduce_none, + 1, 38, :_reduce_none, + 3, 38, :_reduce_none, + 1, 46, :_reduce_none, + 1, 46, :_reduce_none, + 1, 46, :_reduce_none, + 1, 39, :_reduce_none, + 2, 39, :_reduce_47, + 1, 64, :_reduce_48, + 3, 64, :_reduce_49, + 1, 68, :_reduce_none, + 1, 68, :_reduce_none, + 1, 69, :_reduce_52, + 3, 69, :_reduce_53, + 1, 47, :_reduce_none, + 1, 47, :_reduce_none, + 2, 47, :_reduce_56, + 2, 67, :_reduce_none, + 3, 65, :_reduce_58, + 2, 65, :_reduce_59, + 1, 70, :_reduce_60, + 2, 70, :_reduce_61, + 4, 62, :_reduce_62, + 3, 62, :_reduce_63, + 2, 72, :_reduce_none, + 2, 73, :_reduce_65, + 4, 73, :_reduce_66, + 3, 63, :_reduce_67, + 1, 63, :_reduce_68, + 1, 74, :_reduce_none, + 2, 74, :_reduce_70, + 1, 71, :_reduce_71, + 3, 71, :_reduce_72, + 1, 59, :_reduce_73, + 3, 59, :_reduce_74, + 1, 76, :_reduce_75, + 2, 76, :_reduce_76, + 1, 75, :_reduce_none, + 1, 75, :_reduce_none, + 1, 75, :_reduce_none, + 1, 77, :_reduce_none, + 1, 77, :_reduce_none, + 1, 77, :_reduce_none, + 1, 66, :_reduce_none, + 2, 66, :_reduce_none, + 3, 60, :_reduce_85, + 1, 40, :_reduce_86, + 3, 40, :_reduce_87, + 1, 79, :_reduce_none, + 2, 79, :_reduce_89, + 1, 41, :_reduce_90, + 2, 41, :_reduce_91, + 3, 42, :_reduce_92, + 5, 43, :_reduce_93, + 3, 43, :_reduce_94, + 0, 80, :_reduce_95, + 5, 80, :_reduce_96, + 1, 82, :_reduce_none, + 1, 82, :_reduce_none, + 1, 44, :_reduce_99, + 3, 45, :_reduce_100, + 0, 81, :_reduce_none, + 1, 81, :_reduce_none, + 1, 78, :_reduce_none, + 1, 78, :_reduce_none, + 1, 78, :_reduce_none, + 1, 78, :_reduce_none, + 1, 78, :_reduce_none, + 1, 78, :_reduce_none, + 1, 78, :_reduce_none ] + +racc_reduce_n = 110 + +racc_shift_n = 168 + +racc_action_table = [ + -70, -69, 23, 25, 146, 147, 29, 31, 105, 106, + 16, 17, 20, 22, 136, 27, -70, -69, 32, 101, + -70, -69, 154, 100, 113, 115, -70, -69, -70, 109, + 75, 23, 25, 101, 155, 29, 31, 142, 143, 16, + 17, 20, 22, 107, 27, 23, 25, 32, 98, 29, + 31, 96, 94, 16, 17, 20, 22, 78, 27, 23, + 25, 32, 112, 29, 31, 74, 91, 16, 17, 20, + 22, 88, 117, 92, 81, 32, 23, 25, 80, 123, + 29, 31, 100, 125, 16, 17, 20, 22, 126, 23, + 25, 109, 32, 29, 31, 91, 128, 16, 17, 20, + 22, 129, 27, 23, 25, 32, 101, 29, 31, 101, + 130, 16, 17, 20, 22, 79, 52, 23, 25, 32, + 78, 29, 31, 133, 78, 16, 17, 20, 22, 77, + 23, 25, 75, 32, 29, 31, 65, 62, 16, 17, + 20, 22, 139, 23, 25, 101, 32, 29, 31, 60, + 100, 16, 17, 20, 22, 44, 27, 101, 148, 32, + 23, 25, 120, 149, 29, 31, 152, 153, 16, 17, + 20, 22, 42, 27, 157, 159, 32, 23, 25, 120, + 40, 29, 31, 15, 164, 16, 17, 20, 22, 40, + 27, 23, 25, 32, 68, 29, 31, 166, 167, 16, + 17, 20, 22, nil, 27, 23, 25, 32, nil, 29, + 31, 74, nil, 16, 17, 20, 22, nil, 23, 25, + nil, 32, 29, 31, nil, nil, 16, 17, 20, 22, + nil, 23, 25, nil, 32, 29, 31, nil, nil, 16, + 17, 20, 22, nil, 23, 25, nil, 32, 29, 31, + nil, nil, 16, 17, 20, 22, nil, 23, 25, nil, + 32, 29, 31, nil, nil, 16, 17, 20, 22, nil, + 27, 23, 25, 32, nil, 29, 31, nil, nil, 16, + 17, 20, 22, nil, 23, 25, nil, 32, 29, 31, + nil, nil, 16, 17, 20, 22, nil, 23, 25, nil, + 32, 29, 31, nil, nil, 16, 17, 20, 22, nil, + 84, 25, nil, 32, 29, 31, nil, 87, 16, 17, + 20, 22, 4, 6, 7, 8, 9, 10, 11, 12, + 13, 1, 2, 3, 84, 25, nil, nil, 29, 31, + nil, 87, 16, 17, 20, 22, 84, 25, nil, nil, + 29, 31, nil, 87, 16, 17, 20, 22, 84, 25, + nil, nil, 29, 31, nil, 87, 16, 17, 20, 22, + 84, 25, nil, nil, 29, 31, nil, 87, 16, 17, + 20, 22, 84, 25, nil, nil, 29, 31, nil, 87, + 16, 17, 20, 22, 84, 25, nil, nil, 29, 31, + nil, 87, 16, 17, 20, 22 ] + +racc_action_check = [ + 75, 28, 68, 68, 136, 136, 68, 68, 72, 72, + 68, 68, 68, 68, 126, 68, 75, 28, 68, 67, + 75, 28, 143, 66, 86, 86, 75, 28, 75, 75, + 28, 3, 3, 86, 143, 3, 3, 134, 134, 3, + 3, 3, 3, 73, 3, 152, 152, 3, 62, 152, + 152, 60, 56, 152, 152, 152, 152, 51, 152, 52, + 52, 152, 80, 52, 52, 52, 50, 52, 52, 52, + 52, 45, 89, 52, 42, 52, 71, 71, 41, 96, + 71, 71, 97, 98, 71, 71, 71, 71, 100, 7, + 7, 101, 71, 7, 7, 102, 104, 7, 7, 7, + 7, 105, 7, 8, 8, 7, 108, 8, 8, 111, + 112, 8, 8, 8, 8, 40, 8, 9, 9, 8, + 36, 9, 9, 117, 121, 9, 9, 9, 9, 33, + 10, 10, 70, 9, 10, 10, 13, 12, 10, 10, + 10, 10, 130, 2, 2, 131, 10, 2, 2, 11, + 135, 2, 2, 2, 2, 6, 2, 138, 139, 2, + 90, 90, 90, 140, 90, 90, 141, 142, 90, 90, + 90, 90, 5, 90, 148, 151, 90, 127, 127, 127, + 4, 127, 127, 1, 157, 127, 127, 127, 127, 159, + 127, 26, 26, 127, 26, 26, 26, 163, 164, 26, + 26, 26, 26, nil, 26, 27, 27, 26, nil, 27, + 27, 27, nil, 27, 27, 27, 27, nil, 155, 155, + nil, 27, 155, 155, nil, nil, 155, 155, 155, 155, + nil, 122, 122, nil, 155, 122, 122, nil, nil, 122, + 122, 122, 122, nil, 76, 76, nil, 122, 76, 76, + nil, nil, 76, 76, 76, 76, nil, 38, 38, nil, + 76, 38, 38, nil, nil, 38, 38, 38, 38, nil, + 38, 55, 55, 38, nil, 55, 55, nil, nil, 55, + 55, 55, 55, nil, 94, 94, nil, 55, 94, 94, + nil, nil, 94, 94, 94, 94, nil, 59, 59, nil, + 94, 59, 59, nil, nil, 59, 59, 59, 59, nil, + 114, 114, nil, 59, 114, 114, nil, 114, 114, 114, + 114, 114, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 77, 77, nil, nil, 77, 77, + nil, 77, 77, 77, 77, 77, 44, 44, nil, nil, + 44, 44, nil, 44, 44, 44, 44, 44, 113, 113, + nil, nil, 113, 113, nil, 113, 113, 113, 113, 113, + 88, 88, nil, nil, 88, 88, nil, 88, 88, 88, + 88, 88, 74, 74, nil, nil, 74, 74, nil, 74, + 74, 74, 74, 74, 129, 129, nil, nil, 129, 129, + nil, 129, 129, 129, 129, 129 ] + +racc_action_pointer = [ + 320, 152, 129, 17, 165, 172, 137, 75, 89, 103, + 116, 135, 106, 105, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, 177, 191, 1, nil, + nil, nil, nil, 109, nil, nil, 94, nil, 243, nil, + 99, 64, 74, nil, 332, 52, nil, nil, nil, nil, + 50, 31, 45, nil, nil, 257, 36, nil, nil, 283, + 22, nil, 16, nil, nil, nil, -3, -10, -12, nil, + 103, 62, -8, 15, 368, 0, 230, 320, nil, nil, + 47, nil, nil, nil, nil, nil, 4, nil, 356, 50, + 146, nil, nil, nil, 270, nil, 65, 56, 52, nil, + 57, 62, 79, nil, 68, 81, nil, nil, 77, nil, + nil, 80, 96, 344, 296, nil, nil, 108, nil, nil, + nil, 98, 217, nil, nil, nil, -19, 163, nil, 380, + 128, 116, nil, nil, 14, 124, -26, nil, 128, 141, + 148, 141, 152, 7, nil, nil, nil, nil, 160, nil, + nil, 149, 31, nil, nil, 204, nil, 167, nil, 174, + nil, nil, nil, 169, 184, nil, nil, nil ] + +racc_action_default = [ + -110, -110, -110, -110, -14, -110, -20, -110, -110, -110, + -110, -110, -110, -110, -10, -95, -106, -107, -77, -44, + -108, -11, -109, -79, -43, -103, -110, -110, -60, -104, + -55, -105, -78, -68, -54, -71, -45, -12, -110, -1, + -110, -110, -110, -2, -110, -22, -51, -48, -50, -3, + -40, -41, -110, -46, -4, -86, -5, -88, -6, -90, + -110, -7, -95, -8, -9, -99, -101, -61, -59, -56, + -69, -110, -110, -110, -110, -75, -110, -110, -57, -15, + -110, 168, -73, -80, -82, -21, -24, -81, -110, -27, + -110, -83, -47, -89, -110, -91, -110, -101, -110, -100, + -102, -75, -58, -52, -110, -110, -64, -63, -65, -76, + -72, -67, -110, -110, -110, -26, -23, -110, -29, -49, + -84, -42, -87, -92, -94, -95, -110, -110, -62, -110, + -110, -25, -74, -28, -31, -101, -110, -53, -66, -110, + -110, -34, -110, -110, -93, -96, -98, -97, -110, -18, + -13, -38, -110, -30, -33, -110, -32, -16, -19, -14, + -35, -36, -37, -110, -110, -39, -85, -17 ] + +racc_goto_table = [ + 39, 67, 70, 73, 24, 37, 69, 66, 36, 38, + 57, 59, 55, 67, 108, 83, 90, 111, 69, 99, + 85, 49, 53, 76, 158, 134, 141, 70, 73, 151, + 118, 89, 45, 156, 160, 150, 140, 21, 14, 19, + 119, 102, 64, 63, 61, 83, 70, 104, 83, 58, + 124, 132, 56, 131, 97, 54, 93, 43, 5, 83, + 95, 145, 76, nil, 116, 76, nil, nil, 127, 138, + 103, nil, nil, nil, 38, nil, nil, 110, nil, nil, + nil, nil, nil, nil, 83, 83, nil, nil, 144, nil, + nil, nil, nil, nil, nil, 57, 121, 122, nil, nil, + 83, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, 135, nil, nil, + nil, nil, nil, 93, nil, nil, nil, 70, 162, 137, + 70, 163, 161, 38, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, 165 ] + +racc_goto_check = [ + 2, 37, 37, 29, 13, 13, 28, 46, 31, 36, + 41, 41, 45, 37, 25, 44, 32, 25, 28, 47, + 24, 4, 4, 42, 23, 20, 21, 37, 29, 22, + 19, 18, 17, 26, 27, 16, 15, 12, 11, 33, + 34, 35, 10, 9, 8, 44, 37, 29, 44, 7, + 47, 43, 6, 25, 46, 5, 41, 3, 1, 44, + 41, 48, 42, nil, 24, 42, nil, nil, 32, 25, + 13, nil, nil, nil, 36, nil, nil, 41, nil, nil, + nil, nil, nil, nil, 44, 44, nil, nil, 47, nil, + nil, nil, nil, nil, nil, 41, 31, 45, nil, nil, + 44, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, 46, nil, nil, + nil, nil, nil, 41, nil, nil, nil, 37, 29, 13, + 37, 29, 28, 36, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, 2 ] + +racc_goto_pointer = [ + nil, 58, -4, 51, 14, 47, 43, 39, 33, 31, + 29, 37, 35, 2, nil, -94, -105, 26, -14, -59, + -93, -108, -112, -127, -24, -60, -110, -118, -20, -24, + nil, 6, -34, 37, -50, -27, 6, -25, nil, nil, + nil, 1, -5, -63, -29, 3, -8, -47, -75 ] + +racc_goto_default = [ + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, 48, 41, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, 86, nil, nil, 30, 34, + 50, 51, nil, 46, 47, nil, 26, 28, 71, 72, + 33, 35, 114, 82, 18, nil, nil, nil, nil ] + +racc_token_table = { + false => 0, + Object.new => 1, + :DATETIME => 2, + :RECEIVED => 3, + :MADDRESS => 4, + :RETPATH => 5, + :KEYWORDS => 6, + :ENCRYPTED => 7, + :MIMEVERSION => 8, + :CTYPE => 9, + :CENCODING => 10, + :CDISPOSITION => 11, + :ADDRESS => 12, + :MAILBOX => 13, + :DIGIT => 14, + :ATOM => 15, + "," => 16, + ":" => 17, + :FROM => 18, + :BY => 19, + "@" => 20, + :DOMLIT => 21, + :VIA => 22, + :WITH => 23, + :ID => 24, + :FOR => 25, + ";" => 26, + "<" => 27, + ">" => 28, + "." => 29, + :QUOTED => 30, + :TOKEN => 31, + "/" => 32, + "=" => 33 } + +racc_use_result_var = false + +racc_nt_base = 34 + +Racc_arg = [ + racc_action_table, + racc_action_check, + racc_action_default, + racc_action_pointer, + racc_goto_table, + racc_goto_check, + racc_goto_default, + racc_goto_pointer, + racc_nt_base, + racc_reduce_table, + racc_token_table, + racc_shift_n, + racc_reduce_n, + racc_use_result_var ] + +Racc_token_to_s_table = [ +'$end', +'error', +'DATETIME', +'RECEIVED', +'MADDRESS', +'RETPATH', +'KEYWORDS', +'ENCRYPTED', +'MIMEVERSION', +'CTYPE', +'CENCODING', +'CDISPOSITION', +'ADDRESS', +'MAILBOX', +'DIGIT', +'ATOM', +'","', +'":"', +'FROM', +'BY', +'"@"', +'DOMLIT', +'VIA', +'WITH', +'ID', +'FOR', +'";"', +'"<"', +'">"', +'"."', +'QUOTED', +'TOKEN', +'"/"', +'"="', +'$start', +'content', +'datetime', +'received', +'addrs_TOP', +'retpath', +'keys', +'enc', +'version', +'ctype', +'cencode', +'cdisp', +'addr_TOP', +'mbox', +'day', +'hour', +'zone', +'from', +'by', +'via', +'with', +'id', +'for', +'received_datetime', +'received_domain', +'domain', +'msgid', +'received_addrspec', +'routeaddr', +'spec', +'addrs', +'group_bare', +'commas', +'group', +'addr', +'mboxes', +'addr_phrase', +'local_head', +'routes', +'at_domains', +'local', +'word', +'dots', +'domword', +'atom', +'phrase', +'params', +'opt_semicolon', +'value'] + +Racc_debug_parser = false + +##### racc system variables end ##### + + # reduce 0 omitted + +module_eval <<'.,.,', 'parser.y', 16 + def _reduce_1( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 17 + def _reduce_2( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 18 + def _reduce_3( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 19 + def _reduce_4( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 20 + def _reduce_5( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 21 + def _reduce_6( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 22 + def _reduce_7( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 23 + def _reduce_8( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 24 + def _reduce_9( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 25 + def _reduce_10( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 26 + def _reduce_11( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 27 + def _reduce_12( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 33 + def _reduce_13( val, _values) + t = Time.gm(val[3].to_i, val[2], val[1].to_i, 0, 0, 0) + (t + val[4] - val[5]).localtime + end +.,., + + # reduce 14 omitted + + # reduce 15 omitted + +module_eval <<'.,.,', 'parser.y', 42 + def _reduce_16( val, _values) + (val[0].to_i * 60 * 60) + + (val[2].to_i * 60) + end +.,., + +module_eval <<'.,.,', 'parser.y', 47 + def _reduce_17( val, _values) + (val[0].to_i * 60 * 60) + + (val[2].to_i * 60) + + (val[4].to_i) + end +.,., + +module_eval <<'.,.,', 'parser.y', 54 + def _reduce_18( val, _values) + timezone_string_to_unixtime(val[0]) + end +.,., + +module_eval <<'.,.,', 'parser.y', 59 + def _reduce_19( val, _values) + val + end +.,., + + # reduce 20 omitted + +module_eval <<'.,.,', 'parser.y', 65 + def _reduce_21( val, _values) + val[1] + end +.,., + + # reduce 22 omitted + +module_eval <<'.,.,', 'parser.y', 71 + def _reduce_23( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 77 + def _reduce_24( val, _values) + join_domain(val[0]) + end +.,., + +module_eval <<'.,.,', 'parser.y', 81 + def _reduce_25( val, _values) + join_domain(val[2]) + end +.,., + +module_eval <<'.,.,', 'parser.y', 85 + def _reduce_26( val, _values) + join_domain(val[0]) + end +.,., + + # reduce 27 omitted + +module_eval <<'.,.,', 'parser.y', 91 + def _reduce_28( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 96 + def _reduce_29( val, _values) + [] + end +.,., + +module_eval <<'.,.,', 'parser.y', 100 + def _reduce_30( val, _values) + val[0].push val[2] + val[0] + end +.,., + + # reduce 31 omitted + +module_eval <<'.,.,', 'parser.y', 107 + def _reduce_32( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 111 + def _reduce_33( val, _values) + val[1] + end +.,., + + # reduce 34 omitted + +module_eval <<'.,.,', 'parser.y', 117 + def _reduce_35( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 123 + def _reduce_36( val, _values) + val[0].spec + end +.,., + +module_eval <<'.,.,', 'parser.y', 127 + def _reduce_37( val, _values) + val[0].spec + end +.,., + + # reduce 38 omitted + +module_eval <<'.,.,', 'parser.y', 134 + def _reduce_39( val, _values) + val[1] + end +.,., + + # reduce 40 omitted + + # reduce 41 omitted + + # reduce 42 omitted + + # reduce 43 omitted + + # reduce 44 omitted + + # reduce 45 omitted + + # reduce 46 omitted + +module_eval <<'.,.,', 'parser.y', 146 + def _reduce_47( val, _values) + [ Address.new(nil, nil) ] + end +.,., + +module_eval <<'.,.,', 'parser.y', 148 + def _reduce_48( val, _values) + val + end +.,., + +module_eval <<'.,.,', 'parser.y', 149 + def _reduce_49( val, _values) + val[0].push val[2]; val[0] + end +.,., + + # reduce 50 omitted + + # reduce 51 omitted + +module_eval <<'.,.,', 'parser.y', 156 + def _reduce_52( val, _values) + val + end +.,., + +module_eval <<'.,.,', 'parser.y', 160 + def _reduce_53( val, _values) + val[0].push val[2] + val[0] + end +.,., + + # reduce 54 omitted + + # reduce 55 omitted + +module_eval <<'.,.,', 'parser.y', 168 + def _reduce_56( val, _values) + val[1].phrase = Decoder.decode(val[0]) + val[1] + end +.,., + + # reduce 57 omitted + +module_eval <<'.,.,', 'parser.y', 176 + def _reduce_58( val, _values) + AddressGroup.new(val[0], val[2]) + end +.,., + +module_eval <<'.,.,', 'parser.y', 178 + def _reduce_59( val, _values) + AddressGroup.new(val[0], []) + end +.,., + +module_eval <<'.,.,', 'parser.y', 181 + def _reduce_60( val, _values) + val[0].join('.') + end +.,., + +module_eval <<'.,.,', 'parser.y', 182 + def _reduce_61( val, _values) + val[0] << ' ' << val[1].join('.') + end +.,., + +module_eval <<'.,.,', 'parser.y', 186 + def _reduce_62( val, _values) + val[2].routes.replace val[1] + val[2] + end +.,., + +module_eval <<'.,.,', 'parser.y', 191 + def _reduce_63( val, _values) + val[1] + end +.,., + + # reduce 64 omitted + +module_eval <<'.,.,', 'parser.y', 196 + def _reduce_65( val, _values) + [ val[1].join('.') ] + end +.,., + +module_eval <<'.,.,', 'parser.y', 197 + def _reduce_66( val, _values) + val[0].push val[3].join('.'); val[0] + end +.,., + +module_eval <<'.,.,', 'parser.y', 199 + def _reduce_67( val, _values) + Address.new( val[0], val[2] ) + end +.,., + +module_eval <<'.,.,', 'parser.y', 200 + def _reduce_68( val, _values) + Address.new( val[0], nil ) + end +.,., + + # reduce 69 omitted + +module_eval <<'.,.,', 'parser.y', 203 + def _reduce_70( val, _values) + val[0].push ''; val[0] + end +.,., + +module_eval <<'.,.,', 'parser.y', 206 + def _reduce_71( val, _values) + val + end +.,., + +module_eval <<'.,.,', 'parser.y', 209 + def _reduce_72( val, _values) + val[1].times do + val[0].push '' + end + val[0].push val[2] + val[0] + end +.,., + +module_eval <<'.,.,', 'parser.y', 217 + def _reduce_73( val, _values) + val + end +.,., + +module_eval <<'.,.,', 'parser.y', 220 + def _reduce_74( val, _values) + val[1].times do + val[0].push '' + end + val[0].push val[2] + val[0] + end +.,., + +module_eval <<'.,.,', 'parser.y', 227 + def _reduce_75( val, _values) + 0 + end +.,., + +module_eval <<'.,.,', 'parser.y', 228 + def _reduce_76( val, _values) + 1 + end +.,., + + # reduce 77 omitted + + # reduce 78 omitted + + # reduce 79 omitted + + # reduce 80 omitted + + # reduce 81 omitted + + # reduce 82 omitted + + # reduce 83 omitted + + # reduce 84 omitted + +module_eval <<'.,.,', 'parser.y', 243 + def _reduce_85( val, _values) + val[1] = val[1].spec + val.join('') + end +.,., + +module_eval <<'.,.,', 'parser.y', 247 + def _reduce_86( val, _values) + val + end +.,., + +module_eval <<'.,.,', 'parser.y', 248 + def _reduce_87( val, _values) + val[0].push val[2]; val[0] + end +.,., + + # reduce 88 omitted + +module_eval <<'.,.,', 'parser.y', 251 + def _reduce_89( val, _values) + val[0] << ' ' << val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 255 + def _reduce_90( val, _values) + val.push nil + val + end +.,., + +module_eval <<'.,.,', 'parser.y', 260 + def _reduce_91( val, _values) + val + end +.,., + +module_eval <<'.,.,', 'parser.y', 265 + def _reduce_92( val, _values) + [ val[0].to_i, val[2].to_i ] + end +.,., + +module_eval <<'.,.,', 'parser.y', 270 + def _reduce_93( val, _values) + [ val[0].downcase, val[2].downcase, decode_params(val[3]) ] + end +.,., + +module_eval <<'.,.,', 'parser.y', 274 + def _reduce_94( val, _values) + [ val[0].downcase, nil, decode_params(val[1]) ] + end +.,., + +module_eval <<'.,.,', 'parser.y', 279 + def _reduce_95( val, _values) + {} + end +.,., + +module_eval <<'.,.,', 'parser.y', 283 + def _reduce_96( val, _values) + val[0][ val[2].downcase ] = val[4] + val[0] + end +.,., + + # reduce 97 omitted + + # reduce 98 omitted + +module_eval <<'.,.,', 'parser.y', 292 + def _reduce_99( val, _values) + val[0].downcase + end +.,., + +module_eval <<'.,.,', 'parser.y', 297 + def _reduce_100( val, _values) + [ val[0].downcase, decode_params(val[1]) ] + end +.,., + + # reduce 101 omitted + + # reduce 102 omitted + + # reduce 103 omitted + + # reduce 104 omitted + + # reduce 105 omitted + + # reduce 106 omitted + + # reduce 107 omitted + + # reduce 108 omitted + + # reduce 109 omitted + + def _reduce_none( val, _values) + val[0] + end + + end # class Parser + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/port.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/port.rb new file mode 100755 index 00000000..f973c05b --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/port.rb @@ -0,0 +1,377 @@ +# +# port.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/stringio' + + +module TMail + + class Port + def reproducible? + false + end + end + + + ### + ### FilePort + ### + + class FilePort < Port + + def initialize( fname ) + @filename = File.expand_path(fname) + super() + end + + attr_reader :filename + + alias ident filename + + def ==( other ) + other.respond_to?(:filename) and @filename == other.filename + end + + alias eql? == + + def hash + @filename.hash + end + + def inspect + "#<#{self.class}:#{@filename}>" + end + + def reproducible? + true + end + + def size + File.size @filename + end + + + def ropen( &block ) + File.open(@filename, &block) + end + + def wopen( &block ) + File.open(@filename, 'w', &block) + end + + def aopen( &block ) + File.open(@filename, 'a', &block) + end + + + def read_all + ropen {|f| + return f.read + } + end + + + def remove + File.unlink @filename + end + + def move_to( port ) + begin + File.link @filename, port.filename + rescue Errno::EXDEV + copy_to port + end + File.unlink @filename + end + + alias mv move_to + + def copy_to( port ) + if FilePort === port + copy_file @filename, port.filename + else + File.open(@filename) {|r| + port.wopen {|w| + while s = r.sysread(4096) + w.write << s + end + } } + end + end + + alias cp copy_to + + private + + # from fileutils.rb + def copy_file( src, dest ) + st = r = w = nil + + File.open(src, 'rb') {|r| + File.open(dest, 'wb') {|w| + st = r.stat + begin + while true + w.write r.sysread(st.blksize) + end + rescue EOFError + end + } } + end + + end + + + module MailFlags + + def seen=( b ) + set_status 'S', b + end + + def seen? + get_status 'S' + end + + def replied=( b ) + set_status 'R', b + end + + def replied? + get_status 'R' + end + + def flagged=( b ) + set_status 'F', b + end + + def flagged? + get_status 'F' + end + + private + + def procinfostr( str, tag, true_p ) + a = str.upcase.split(//) + a.push true_p ? tag : nil + a.delete tag unless true_p + a.compact.sort.join('').squeeze + end + + end + + + class MhPort < FilePort + + include MailFlags + + private + + def set_status( tag, flag ) + begin + tmpfile = @filename + '.tmailtmp.' + $$.to_s + File.open(tmpfile, 'w') {|f| + write_status f, tag, flag + } + File.unlink @filename + File.link tmpfile, @filename + ensure + File.unlink tmpfile + end + end + + def write_status( f, tag, flag ) + stat = '' + File.open(@filename) {|r| + while line = r.gets + if line.strip.empty? + break + elsif m = /\AX-TMail-Status:/i.match(line) + stat = m.post_match.strip + else + f.print line + end + end + + s = procinfostr(stat, tag, flag) + f.puts 'X-TMail-Status: ' + s unless s.empty? + f.puts + + while s = r.read(2048) + f.write s + end + } + end + + def get_status( tag ) + File.foreach(@filename) {|line| + return false if line.strip.empty? + if m = /\AX-TMail-Status:/i.match(line) + return m.post_match.strip.include?(tag[0]) + end + } + false + end + + end + + + class MaildirPort < FilePort + + def move_to_new + new = replace_dir(@filename, 'new') + File.rename @filename, new + @filename = new + end + + def move_to_cur + new = replace_dir(@filename, 'cur') + File.rename @filename, new + @filename = new + end + + def replace_dir( path, dir ) + "#{File.dirname File.dirname(path)}/#{dir}/#{File.basename path}" + end + private :replace_dir + + + include MailFlags + + private + + MAIL_FILE = /\A(\d+\.[\d_]+\.[^:]+)(?:\:(\d),(\w+)?)?\z/ + + def set_status( tag, flag ) + if m = MAIL_FILE.match(File.basename(@filename)) + s, uniq, type, info, = m.to_a + return if type and type != '2' # do not change anything + newname = File.dirname(@filename) + '/' + + uniq + ':2,' + procinfostr(info.to_s, tag, flag) + else + newname = @filename + ':2,' + tag + end + + File.link @filename, newname + File.unlink @filename + @filename = newname + end + + def get_status( tag ) + m = MAIL_FILE.match(File.basename(@filename)) or return false + m[2] == '2' and m[3].to_s.include?(tag[0]) + end + + end + + + ### + ### StringPort + ### + + class StringPort < Port + + def initialize( str = '' ) + @buffer = str + super() + end + + def string + @buffer + end + + def to_s + @buffer.dup + end + + alias read_all to_s + + def size + @buffer.size + end + + def ==( other ) + StringPort === other and @buffer.equal? other.string + end + + alias eql? == + + def hash + @buffer.object_id.hash + end + + def inspect + "#<#{self.class}:id=#{sprintf '0x%x', @buffer.object_id}>" + end + + def reproducible? + true + end + + def ropen( &block ) + @buffer or raise Errno::ENOENT, "#{inspect} is already removed" + StringInput.open(@buffer, &block) + end + + def wopen( &block ) + @buffer = '' + StringOutput.new(@buffer, &block) + end + + def aopen( &block ) + @buffer ||= '' + StringOutput.new(@buffer, &block) + end + + def remove + @buffer = nil + end + + alias rm remove + + def copy_to( port ) + port.wopen {|f| + f.write @buffer + } + end + + alias cp copy_to + + def move_to( port ) + if StringPort === port + str = @buffer + port.instance_eval { @buffer = str } + else + copy_to port + end + remove + end + + end + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/quoting.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/quoting.rb new file mode 100644 index 00000000..a56e2267 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/quoting.rb @@ -0,0 +1,125 @@ +module TMail + class Mail + def subject(to_charset = 'utf-8') + Unquoter.unquote_and_convert_to(quoted_subject, to_charset) + end + + def unquoted_body(to_charset = 'utf-8') + from_charset = sub_header("content-type", "charset") + case (content_transfer_encoding || "7bit").downcase + when "quoted-printable" + Unquoter.unquote_quoted_printable_and_convert_to(quoted_body, + to_charset, from_charset, true) + when "base64" + Unquoter.unquote_base64_and_convert_to(quoted_body, to_charset, + from_charset) + when "7bit", "8bit" + Unquoter.convert_to(quoted_body, to_charset, from_charset) + when "binary" + quoted_body + else + quoted_body + end + end + + def body(to_charset = 'utf-8', &block) + attachment_presenter = block || Proc.new { |file_name| "Attachment: #{file_name}\n" } + + if multipart? + parts.collect { |part| + header = part["content-type"] + + if part.multipart? + part.body(to_charset, &attachment_presenter) + elsif header.nil? + "" + elsif !attachment?(part) + part.unquoted_body(to_charset) + else + attachment_presenter.call(header["name"] || "(unnamed)") + end + }.join + else + unquoted_body(to_charset) + end + end + end + + class Unquoter + class << self + def unquote_and_convert_to(text, to_charset, from_charset = "iso-8859-1", preserve_underscores=false) + return "" if text.nil? + if text =~ /^=\?(.*?)\?(.)\?(.*)\?=$/ + from_charset = $1 + quoting_method = $2 + text = $3 + case quoting_method.upcase + when "Q" then + unquote_quoted_printable_and_convert_to(text, to_charset, from_charset, preserve_underscores) + when "B" then + unquote_base64_and_convert_to(text, to_charset, from_charset) + else + raise "unknown quoting method #{quoting_method.inspect}" + end + else + convert_to(text, to_charset, from_charset) + end + end + + def unquote_quoted_printable_and_convert_to(text, to, from, preserve_underscores=false) + text = text.gsub(/_/, " ") unless preserve_underscores + convert_to(text.unpack("M*").first, to, from) + end + + def unquote_base64_and_convert_to(text, to, from) + convert_to(Base64.decode(text).first, to, from) + end + + begin + require 'iconv' + def convert_to(text, to, from) + return text unless to && from + text ? Iconv.iconv(to, from, text).first : "" + rescue Iconv::IllegalSequence, Errno::EINVAL + # the 'from' parameter specifies a charset other than what the text + # actually is...not much we can do in this case but just return the + # unconverted text. + # + # Ditto if either parameter represents an unknown charset, like + # X-UNKNOWN. + text + end + rescue LoadError + # Not providing quoting support + def convert_to(text, to, from) + warn "Action Mailer: iconv not loaded; ignoring conversion from #{from} to #{to} (#{__FILE__}:#{__LINE__})" + text + end + end + end + end +end + +if __FILE__ == $0 + require 'test/unit' + + class TC_Unquoter < Test::Unit::TestCase + def test_unquote_quoted_printable + a ="=?ISO-8859-1?Q?[166417]_Bekr=E6ftelse_fra_Rejsefeber?=" + b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8') + assert_equal "[166417] Bekr\303\246ftelse fra Rejsefeber", b + end + + def test_unquote_base64 + a ="=?ISO-8859-1?B?WzE2NjQxN10gQmVrcuZmdGVsc2UgZnJhIFJlanNlZmViZXI=?=" + b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8') + assert_equal "[166417] Bekr\303\246ftelse fra Rejsefeber", b + end + + def test_unquote_without_charset + a ="[166417]_Bekr=E6ftelse_fra_Rejsefeber" + b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8') + assert_equal "[166417]_Bekr=E6ftelse_fra_Rejsefeber", b + end + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/scanner.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/scanner.rb new file mode 100755 index 00000000..839dd793 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/scanner.rb @@ -0,0 +1,41 @@ +# +# scanner.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/utils' + +module TMail + require 'tmail/scanner_r.rb' + begin + raise LoadError, 'Turn off Ruby extention by user choice' if ENV['NORUBYEXT'] + require 'tmail/scanner_c.so' + Scanner = Scanner_C + rescue LoadError + Scanner = Scanner_R + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/scanner_r.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/scanner_r.rb new file mode 100755 index 00000000..ccf576c2 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/scanner_r.rb @@ -0,0 +1,263 @@ +# +# scanner_r.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/config' + + +module TMail + + class Scanner_R + + Version = '0.10.7' + Version.freeze + + MIME_HEADERS = { + :CTYPE => true, + :CENCODING => true, + :CDISPOSITION => true + } + + alnum = 'a-zA-Z0-9' + atomsyms = %q[ _#!$%&`'*+-{|}~^@/=? ].strip + tokensyms = %q[ _#!$%&`'*+-{|}~^@. ].strip + + atomchars = alnum + Regexp.quote(atomsyms) + tokenchars = alnum + Regexp.quote(tokensyms) + iso2022str = '\e(?!\(B)..(?:[^\e]+|\e(?!\(B)..)*\e\(B' + + eucstr = '(?:[\xa1-\xfe][\xa1-\xfe])+' + sjisstr = '(?:[\x81-\x9f\xe0-\xef][\x40-\x7e\x80-\xfc])+' + utf8str = '(?:[\xc0-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf][\x80-\xbf])+' + + quoted_with_iso2022 = /\A(?:[^\\\e"]+|#{iso2022str})+/n + domlit_with_iso2022 = /\A(?:[^\\\e\]]+|#{iso2022str})+/n + comment_with_iso2022 = /\A(?:[^\\\e()]+|#{iso2022str})+/n + + quoted_without_iso2022 = /\A[^\\"]+/n + domlit_without_iso2022 = /\A[^\\\]]+/n + comment_without_iso2022 = /\A[^\\()]+/n + + PATTERN_TABLE = {} + PATTERN_TABLE['EUC'] = + [ + /\A(?:[#{atomchars}]+|#{iso2022str}|#{eucstr})+/n, + /\A(?:[#{tokenchars}]+|#{iso2022str}|#{eucstr})+/n, + quoted_with_iso2022, + domlit_with_iso2022, + comment_with_iso2022 + ] + PATTERN_TABLE['SJIS'] = + [ + /\A(?:[#{atomchars}]+|#{iso2022str}|#{sjisstr})+/n, + /\A(?:[#{tokenchars}]+|#{iso2022str}|#{sjisstr})+/n, + quoted_with_iso2022, + domlit_with_iso2022, + comment_with_iso2022 + ] + PATTERN_TABLE['UTF8'] = + [ + /\A(?:[#{atomchars}]+|#{utf8str})+/n, + /\A(?:[#{tokenchars}]+|#{utf8str})+/n, + quoted_without_iso2022, + domlit_without_iso2022, + comment_without_iso2022 + ] + PATTERN_TABLE['NONE'] = + [ + /\A[#{atomchars}]+/n, + /\A[#{tokenchars}]+/n, + quoted_without_iso2022, + domlit_without_iso2022, + comment_without_iso2022 + ] + + + def initialize( str, scantype, comments ) + init_scanner str + @comments = comments || [] + @debug = false + + # fix scanner mode + @received = (scantype == :RECEIVED) + @is_mime_header = MIME_HEADERS[scantype] + + atom, token, @quoted_re, @domlit_re, @comment_re = PATTERN_TABLE[$KCODE] + @word_re = (MIME_HEADERS[scantype] ? token : atom) + end + + attr_accessor :debug + + def scan( &block ) + if @debug + scan_main do |arr| + s, v = arr + printf "%7d %-10s %s\n", + rest_size(), + s.respond_to?(:id2name) ? s.id2name : s.inspect, + v.inspect + yield arr + end + else + scan_main(&block) + end + end + + private + + RECV_TOKEN = { + 'from' => :FROM, + 'by' => :BY, + 'via' => :VIA, + 'with' => :WITH, + 'id' => :ID, + 'for' => :FOR + } + + def scan_main + until eof? + if skip(/\A[\n\r\t ]+/n) # LWSP + break if eof? + end + + if s = readstr(@word_re) + if @is_mime_header + yield :TOKEN, s + else + # atom + if /\A\d+\z/ === s + yield :DIGIT, s + elsif @received + yield RECV_TOKEN[s.downcase] || :ATOM, s + else + yield :ATOM, s + end + end + + elsif skip(/\A"/) + yield :QUOTED, scan_quoted_word() + + elsif skip(/\A\[/) + yield :DOMLIT, scan_domain_literal() + + elsif skip(/\A\(/) + @comments.push scan_comment() + + else + c = readchar() + yield c, c + end + end + + yield false, '$' + end + + def scan_quoted_word + scan_qstr(@quoted_re, /\A"/, 'quoted-word') + end + + def scan_domain_literal + '[' + scan_qstr(@domlit_re, /\A\]/, 'domain-literal') + ']' + end + + def scan_qstr( pattern, terminal, type ) + result = '' + until eof? + if s = readstr(pattern) then result << s + elsif skip(terminal) then return result + elsif skip(/\A\\/) then result << readchar() + else + raise "TMail FATAL: not match in #{type}" + end + end + scan_error! "found unterminated #{type}" + end + + def scan_comment + result = '' + nest = 1 + content = @comment_re + + until eof? + if s = readstr(content) then result << s + elsif skip(/\A\)/) then nest -= 1 + return result if nest == 0 + result << ')' + elsif skip(/\A\(/) then nest += 1 + result << '(' + elsif skip(/\A\\/) then result << readchar() + else + raise 'TMail FATAL: not match in comment' + end + end + scan_error! 'found unterminated comment' + end + + # string scanner + + def init_scanner( str ) + @src = str + end + + def eof? + @src.empty? + end + + def rest_size + @src.size + end + + def readstr( re ) + if m = re.match(@src) + @src = m.post_match + m[0] + else + nil + end + end + + def readchar + readstr(/\A./) + end + + def skip( re ) + if m = re.match(@src) + @src = m.post_match + true + else + false + end + end + + def scan_error!( msg ) + raise SyntaxError, msg + end + + end + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/stringio.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/stringio.rb new file mode 100755 index 00000000..532be3db --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/stringio.rb @@ -0,0 +1,277 @@ +# +# stringio.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +class StringInput#:nodoc: + + include Enumerable + + class << self + + def new( str ) + if block_given? + begin + f = super + yield f + ensure + f.close if f + end + else + super + end + end + + alias open new + + end + + def initialize( str ) + @src = str + @pos = 0 + @closed = false + @lineno = 0 + end + + attr_reader :lineno + + def string + @src + end + + def inspect + "#<#{self.class}:#{@closed ? 'closed' : 'open'},src=#{@src[0,30].inspect}>" + end + + def close + stream_check! + @pos = nil + @closed = true + end + + def closed? + @closed + end + + def pos + stream_check! + [@pos, @src.size].min + end + + alias tell pos + + def seek( offset, whence = IO::SEEK_SET ) + stream_check! + case whence + when IO::SEEK_SET + @pos = offset + when IO::SEEK_CUR + @pos += offset + when IO::SEEK_END + @pos = @src.size - offset + else + raise ArgumentError, "unknown seek flag: #{whence}" + end + @pos = 0 if @pos < 0 + @pos = [@pos, @src.size + 1].min + offset + end + + def rewind + stream_check! + @pos = 0 + end + + def eof? + stream_check! + @pos > @src.size + end + + def each( &block ) + stream_check! + begin + @src.each(&block) + ensure + @pos = 0 + end + end + + def gets + stream_check! + if idx = @src.index(?\n, @pos) + idx += 1 # "\n".size + line = @src[ @pos ... idx ] + @pos = idx + @pos += 1 if @pos == @src.size + else + line = @src[ @pos .. -1 ] + @pos = @src.size + 1 + end + @lineno += 1 + + line + end + + def getc + stream_check! + ch = @src[@pos] + @pos += 1 + @pos += 1 if @pos == @src.size + ch + end + + def read( len = nil ) + stream_check! + return read_all unless len + str = @src[@pos, len] + @pos += len + @pos += 1 if @pos == @src.size + str + end + + alias sysread read + + def read_all + stream_check! + return nil if eof? + rest = @src[@pos ... @src.size] + @pos = @src.size + 1 + rest + end + + def stream_check! + @closed and raise IOError, 'closed stream' + end + +end + + +class StringOutput#:nodoc: + + class << self + + def new( str = '' ) + if block_given? + begin + f = super + yield f + ensure + f.close if f + end + else + super + end + end + + alias open new + + end + + def initialize( str = '' ) + @dest = str + @closed = false + end + + def close + @closed = true + end + + def closed? + @closed + end + + def string + @dest + end + + alias value string + alias to_str string + + def size + @dest.size + end + + alias pos size + + def inspect + "#<#{self.class}:#{@dest ? 'open' : 'closed'},#{id}>" + end + + def print( *args ) + stream_check! + raise ArgumentError, 'wrong # of argument (0 for >1)' if args.empty? + args.each do |s| + raise ArgumentError, 'nil not allowed' if s.nil? + @dest << s.to_s + end + nil + end + + def puts( *args ) + stream_check! + args.each do |str| + @dest << (s = str.to_s) + @dest << "\n" unless s[-1] == ?\n + end + @dest << "\n" if args.empty? + nil + end + + def putc( ch ) + stream_check! + @dest << ch.chr + nil + end + + def printf( *args ) + stream_check! + @dest << sprintf(*args) + nil + end + + def write( str ) + stream_check! + s = str.to_s + @dest << s + s.size + end + + alias syswrite write + + def <<( str ) + stream_check! + @dest << str.to_s + self + end + + private + + def stream_check! + @closed and raise IOError, 'closed stream' + end + +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/tmail.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/tmail.rb new file mode 100755 index 00000000..57ed3cc5 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/tmail.rb @@ -0,0 +1 @@ +require 'tmail' diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/utils.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/utils.rb new file mode 100755 index 00000000..852acd75 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/utils.rb @@ -0,0 +1,238 @@ +# +# utils.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +module TMail + + class SyntaxError < StandardError; end + + + def TMail.new_boundary + 'mimepart_' + random_tag + end + + def TMail.new_message_id( fqdn = nil ) + fqdn ||= ::Socket.gethostname + "<#{random_tag()}@#{fqdn}.tmail>" + end + + def TMail.random_tag + @uniq += 1 + t = Time.now + sprintf('%x%x_%x%x%d%x', + t.to_i, t.tv_usec, + $$, Thread.current.object_id, @uniq, rand(255)) + end + private_class_method :random_tag + + @uniq = 0 + + + module TextUtils + + aspecial = '()<>[]:;.\\,"' + tspecial = '()<>[];:\\,"/?=' + lwsp = " \t\r\n" + control = '\x00-\x1f\x7f-\xff' + + ATOM_UNSAFE = /[#{Regexp.quote aspecial}#{control}#{lwsp}]/n + PHRASE_UNSAFE = /[#{Regexp.quote aspecial}#{control}]/n + TOKEN_UNSAFE = /[#{Regexp.quote tspecial}#{control}#{lwsp}]/n + CONTROL_CHAR = /[#{control}]/n + + def atom_safe?( str ) + not ATOM_UNSAFE === str + end + + def quote_atom( str ) + (ATOM_UNSAFE === str) ? dquote(str) : str + end + + def quote_phrase( str ) + (PHRASE_UNSAFE === str) ? dquote(str) : str + end + + def token_safe?( str ) + not TOKEN_UNSAFE === str + end + + def quote_token( str ) + (TOKEN_UNSAFE === str) ? dquote(str) : str + end + + def dquote( str ) + '"' + str.gsub(/["\\]/n) {|s| '\\' + s } + '"' + end + private :dquote + + + def join_domain( arr ) + arr.map {|i| + if /\A\[.*\]\z/ === i + i + else + quote_atom(i) + end + }.join('.') + end + + + ZONESTR_TABLE = { + 'jst' => 9 * 60, + 'eet' => 2 * 60, + 'bst' => 1 * 60, + 'met' => 1 * 60, + 'gmt' => 0, + 'utc' => 0, + 'ut' => 0, + 'nst' => -(3 * 60 + 30), + 'ast' => -4 * 60, + 'edt' => -4 * 60, + 'est' => -5 * 60, + 'cdt' => -5 * 60, + 'cst' => -6 * 60, + 'mdt' => -6 * 60, + 'mst' => -7 * 60, + 'pdt' => -7 * 60, + 'pst' => -8 * 60, + 'a' => -1 * 60, + 'b' => -2 * 60, + 'c' => -3 * 60, + 'd' => -4 * 60, + 'e' => -5 * 60, + 'f' => -6 * 60, + 'g' => -7 * 60, + 'h' => -8 * 60, + 'i' => -9 * 60, + # j not use + 'k' => -10 * 60, + 'l' => -11 * 60, + 'm' => -12 * 60, + 'n' => 1 * 60, + 'o' => 2 * 60, + 'p' => 3 * 60, + 'q' => 4 * 60, + 'r' => 5 * 60, + 's' => 6 * 60, + 't' => 7 * 60, + 'u' => 8 * 60, + 'v' => 9 * 60, + 'w' => 10 * 60, + 'x' => 11 * 60, + 'y' => 12 * 60, + 'z' => 0 * 60 + } + + def timezone_string_to_unixtime( str ) + if m = /([\+\-])(\d\d?)(\d\d)/.match(str) + sec = (m[2].to_i * 60 + m[3].to_i) * 60 + m[1] == '-' ? -sec : sec + else + min = ZONESTR_TABLE[str.downcase] or + raise SyntaxError, "wrong timezone format '#{str}'" + min * 60 + end + end + + + WDAY = %w( Sun Mon Tue Wed Thu Fri Sat TMailBUG ) + MONTH = %w( TMailBUG Jan Feb Mar Apr May Jun + Jul Aug Sep Oct Nov Dec TMailBUG ) + + def time2str( tm ) + # [ruby-list:7928] + gmt = Time.at(tm.to_i) + gmt.gmtime + offset = tm.to_i - Time.local(*gmt.to_a[0,6].reverse).to_i + + # DO NOT USE strftime: setlocale() breaks it + sprintf '%s, %s %s %d %02d:%02d:%02d %+.2d%.2d', + WDAY[tm.wday], tm.mday, MONTH[tm.month], + tm.year, tm.hour, tm.min, tm.sec, + *(offset / 60).divmod(60) + end + + + MESSAGE_ID = /<[^\@>]+\@[^>\@]+>/ + + def message_id?( str ) + MESSAGE_ID === str + end + + + MIME_ENCODED = /=\?[^\s?=]+\?[QB]\?[^\s?=]+\?=/i + + def mime_encoded?( str ) + MIME_ENCODED === str + end + + + def decode_params( hash ) + new = Hash.new + encoded = nil + hash.each do |key, value| + if m = /\*(?:(\d+)\*)?\z/.match(key) + ((encoded ||= {})[m.pre_match] ||= [])[(m[1] || 0).to_i] = value + else + new[key] = to_kcode(value) + end + end + if encoded + encoded.each do |key, strings| + new[key] = decode_RFC2231(strings.join('')) + end + end + + new + end + + NKF_FLAGS = { + 'EUC' => '-e -m', + 'SJIS' => '-s -m' + } + + def to_kcode( str ) + flag = NKF_FLAGS[$KCODE] or return str + NKF.nkf(flag, str) + end + + RFC2231_ENCODED = /\A(?:iso-2022-jp|euc-jp|shift_jis|us-ascii)?'[a-z]*'/in + + def decode_RFC2231( str ) + m = RFC2231_ENCODED.match(str) or return str + begin + NKF.nkf(NKF_FLAGS[$KCODE], + m.post_match.gsub(/%[\da-f]{2}/in) {|s| s[1,2].hex.chr }) + rescue + m.post_match.gsub(/%[\da-f]{2}/in, "") + end + end + + end + +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/version.rb b/vendor/rails/actionmailer/lib/action_mailer/version.rb new file mode 100644 index 00000000..21bc3f6a --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/version.rb @@ -0,0 +1,9 @@ +module ActionMailer + module VERSION #:nodoc: + MAJOR = 1 + MINOR = 2 + TINY = 5 + + STRING = [MAJOR, MINOR, TINY].join('.') + end +end diff --git a/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_helper.rhtml b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_helper.rhtml new file mode 100644 index 00000000..378777f8 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_helper.rhtml @@ -0,0 +1 @@ +Hello, <%= person_name %>. Thanks for registering! diff --git a/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_helper_method.rhtml b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_helper_method.rhtml new file mode 100644 index 00000000..d5b8b285 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_helper_method.rhtml @@ -0,0 +1 @@ +This message brought to you by <%= name_of_the_mailer_class %>. diff --git a/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_mail_helper.rhtml b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_mail_helper.rhtml new file mode 100644 index 00000000..96ec49d1 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_mail_helper.rhtml @@ -0,0 +1,5 @@ +From "Romeo and Juliet": + +<%= block_format @text %> + +Good ol' Shakespeare. diff --git a/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_test_helper.rhtml b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_test_helper.rhtml new file mode 100644 index 00000000..52ea9aa4 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_test_helper.rhtml @@ -0,0 +1 @@ +So, <%= test_format(@text) %> diff --git a/vendor/rails/actionmailer/test/fixtures/helpers/test_helper.rb b/vendor/rails/actionmailer/test/fixtures/helpers/test_helper.rb new file mode 100644 index 00000000..f479820c --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/helpers/test_helper.rb @@ -0,0 +1,5 @@ +module TestHelper + def test_format(text) + "#{text}" + end +end diff --git a/vendor/rails/actionmailer/test/fixtures/path.with.dots/multipart_with_template_path_with_dots.rhtml b/vendor/rails/actionmailer/test/fixtures/path.with.dots/multipart_with_template_path_with_dots.rhtml new file mode 100644 index 00000000..897a5065 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/path.with.dots/multipart_with_template_path_with_dots.rhtml @@ -0,0 +1 @@ +Have a lovely picture, from me. Enjoy! \ No newline at end of file diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email b/vendor/rails/actionmailer/test/fixtures/raw_email new file mode 100644 index 00000000..43f7a59c --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email @@ -0,0 +1,14 @@ +From jamis_buck@byu.edu Mon May 2 16:07:05 2005 +Mime-Version: 1.0 (Apple Message framework v622) +Content-Transfer-Encoding: base64 +Message-Id: +Content-Type: text/plain; + charset=EUC-KR; + format=flowed +To: willard15georgina@jamis.backpackit.com +From: Jamis Buck +Subject: =?EUC-KR?Q?NOTE:_=C7=D1=B1=B9=B8=BB=B7=CE_=C7=CF=B4=C2_=B0=CD?= +Date: Mon, 2 May 2005 16:07:05 -0600 + +tOu6zrrQwMcguLbC+bChwfa3ziwgv+y4rrTCIMfPs6q01MC7ILnPvcC0z7TZLg0KDQrBpiDAzLin +wLogSmFtaXPA1LTPtNku diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email10 b/vendor/rails/actionmailer/test/fixtures/raw_email10 new file mode 100644 index 00000000..b1fc2b26 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email10 @@ -0,0 +1,20 @@ +Return-Path: +Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for ; Tue, 10 May 2005 15:27:05 -0500 +Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for ; Tue, 10 May 2005 15:27:04 -0500 +Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for ; Tue, 10 May 2005 15:27:03 -0500 +Date: Tue, 10 May 2005 15:27:03 -0500 +From: xxx@xxxx.xxx +Sender: xxx@xxxx.xxx +To: xxxxxxxxxxx@xxxx.xxxx.xxx +Message-Id: +X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx +Delivered-To: xxx@xxxx.xxx +Importance: normal +Content-Type: text/plain; charset=X-UNKNOWN + +Test test. Hi. Waving. m + +---------------------------------------------------------------- +Sent via Bell Mobility's Text Messaging service. +Envoyé par le service de messagerie texte de Bell Mobilité. +---------------------------------------------------------------- diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email11 b/vendor/rails/actionmailer/test/fixtures/raw_email11 new file mode 100644 index 00000000..8af74b87 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email11 @@ -0,0 +1,34 @@ +From xxx@xxxx.com Wed Apr 27 14:15:31 2005 +Mime-Version: 1.0 (Apple Message framework v619.2) +To: xxxxx@xxxxx +Message-Id: <416eaebec6d333ec6939eaf8a7d80724@xxxxx> +Content-Type: multipart/alternative; + boundary=Apple-Mail-5-1037861608 +From: xxxxx@xxxxx +Subject: worse when you use them. +Date: Wed, 27 Apr 2005 14:15:31 -0700 + + + + +--Apple-Mail-5-1037861608 +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; + charset=US-ASCII; + format=flowed + + +XXXXX Xxxxx + +--Apple-Mail-5-1037861608 +Content-Transfer-Encoding: 7bit +Content-Type: text/enriched; + charset=US-ASCII + + + +XXXXX Xxxxx + + +--Apple-Mail-5-1037861608-- + diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email12 b/vendor/rails/actionmailer/test/fixtures/raw_email12 new file mode 100644 index 00000000..2cd31720 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email12 @@ -0,0 +1,32 @@ +Mime-Version: 1.0 (Apple Message framework v730) +Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151 +Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com> +From: foo@example.com +Subject: testing +Date: Mon, 6 Jun 2005 22:21:22 +0200 +To: blah@example.com + + +--Apple-Mail-13-196941151 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=ISO-8859-1; + delsp=yes; + format=flowed + +This is the first part. + +--Apple-Mail-13-196941151 +Content-Type: image/jpeg +Content-Transfer-Encoding: base64 +Content-Location: Photo25.jpg +Content-ID: +Content-Disposition: inline + +jamisSqGSIb3DQEHAqCAMIjamisxCzAJBgUrDgMCGgUAMIAGCSqGSjamisEHAQAAoIIFSjCCBUYw +ggQujamisQICBD++ukQwDQYJKojamisNAQEFBQAwMTELMAkGA1UEBhMCRjamisAKBgNVBAoTA1RE +QzEUMBIGjamisxMLVERDIE9DRVMgQ0jamisNMDQwMjI5MTE1OTAxWhcNMDYwMjamisIyOTAxWjCB +gDELMAkGA1UEjamisEsxKTAnBgNVBAoTIEjamisuIG9yZ2FuaXNhdG9yaXNrIHRpbjamisRuaW5= + +--Apple-Mail-13-196941151-- + diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email13 b/vendor/rails/actionmailer/test/fixtures/raw_email13 new file mode 100644 index 00000000..7d9314e3 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email13 @@ -0,0 +1,29 @@ +Mime-Version: 1.0 (Apple Message framework v730) +Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151 +Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com> +From: foo@example.com +Subject: testing +Date: Mon, 6 Jun 2005 22:21:22 +0200 +To: blah@example.com + + +--Apple-Mail-13-196941151 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=ISO-8859-1; + delsp=yes; + format=flowed + +This is the first part. + +--Apple-Mail-13-196941151 +Content-Type: text/x-ruby-script; name="hello.rb" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="api.rb" + +puts "Hello, world!" +gets + +--Apple-Mail-13-196941151-- + diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email2 b/vendor/rails/actionmailer/test/fixtures/raw_email2 new file mode 100644 index 00000000..3999fcc8 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email2 @@ -0,0 +1,114 @@ +From xxxxxxxxx.xxxxxxx@gmail.com Sun May 8 19:07:09 2005 +Return-Path: +X-Original-To: xxxxx@xxxxx.xxxxxxxxx.com +Delivered-To: xxxxx@xxxxx.xxxxxxxxx.com +Received: from localhost (localhost [127.0.0.1]) + by xxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 06C9DA98D + for ; Sun, 8 May 2005 19:09:13 +0000 (GMT) +Received: from xxxxx.xxxxxxxxx.com ([127.0.0.1]) + by localhost (xxxxx.xxxxxxxxx.com [127.0.0.1]) (amavisd-new, port 10024) + with LMTP id 88783-08 for ; + Sun, 8 May 2005 19:09:12 +0000 (GMT) +Received: from xxxxxxx.xxxxxxxxx.com (xxxxxxx.xxxxxxxxx.com [69.36.39.150]) + by xxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 10D8BA960 + for ; Sun, 8 May 2005 19:09:12 +0000 (GMT) +Received: from zproxy.gmail.com (zproxy.gmail.com [64.233.162.199]) + by xxxxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 9EBC4148EAB + for ; Sun, 8 May 2005 14:09:11 -0500 (CDT) +Received: by zproxy.gmail.com with SMTP id 13so1233405nzp + for ; Sun, 08 May 2005 12:09:11 -0700 (PDT) +DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws; + s=beta; d=gmail.com; + h=received:message-id:date:from:reply-to:to:subject:in-reply-to:mime-version:content-type:references; + b=cid1mzGEFa3gtRa06oSrrEYfKca2CTKu9sLMkWxjbvCsWMtp9RGEILjUz0L5RySdH5iO661LyNUoHRFQIa57bylAbXM3g2DTEIIKmuASDG3x3rIQ4sHAKpNxP7Pul+mgTaOKBv+spcH7af++QEJ36gHFXD2O/kx9RePs3JNf/K8= +Received: by 10.36.10.16 with SMTP id 16mr1012493nzj; + Sun, 08 May 2005 12:09:11 -0700 (PDT) +Received: by 10.36.5.10 with HTTP; Sun, 8 May 2005 12:09:11 -0700 (PDT) +Message-ID: +Date: Sun, 8 May 2005 14:09:11 -0500 +From: xxxxxxxxx xxxxxxx +Reply-To: xxxxxxxxx xxxxxxx +To: xxxxx xxxx +Subject: Fwd: Signed email causes file attachments +In-Reply-To: +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_Part_5028_7368284.1115579351471" +References: + +------=_Part_5028_7368284.1115579351471 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +We should not include these files or vcards as attachments. + +---------- Forwarded message ---------- +From: xxxxx xxxxxx +Date: May 8, 2005 1:17 PM +Subject: Signed email causes file attachments +To: xxxxxxx@xxxxxxxxxx.com + + +Hi, + +Just started to use my xxxxxxxx account (to set-up a GTD system, +natch) and noticed that when I send content via email the signature/ +certificate from my email account gets added as a file (e.g. +"smime.p7s"). + +Obviously I can uncheck the signature option in the Mail compose +window but how often will I remember to do that? + +Is there any way these kind of files could be ignored, e.g. via some +sort of exclusions list? + +------=_Part_5028_7368284.1115579351471 +Content-Type: application/pkcs7-signature; name=smime.p7s +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7s" + +MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGFDCCAs0w +ggI2oAMCAQICAw5c+TANBgkqhkiG9w0BAQQFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhh +d3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVt +YWlsIElzc3VpbmcgQ0EwHhcNMDUwMzI5MDkzOTEwWhcNMDYwMzI5MDkzOTEwWjBCMR8wHQYDVQQD +ExZUaGF3dGUgRnJlZW1haWwgTWVtYmVyMR8wHQYJKoZIhvcNAQkBFhBzbWhhdW5jaEBtYWMuY29t +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn90dPsYS3LjfMY211OSYrDQLzwNYPlAL +7+/0XA+kdy8/rRnyEHFGwhNCDmg0B6pxC7z3xxJD/8GfCd+IYUUNUQV5m9MkxfP9pTVXZVIYLaBw +o8xS3A0a1LXealcmlEbJibmKkEaoXci3MhryLgpaa+Kk/sH02SNatDO1vS28bPsibZpcc6deFrla +hSYnL+PW54mDTGHIcCN2fbx/Y6qspzqmtKaXrv75NBtuy9cB6KzU4j2xXbTkAwz3pRSghJJaAwdp ++yIivAD3vr0kJE3p+Ez34HMh33EXEpFoWcN+MCEQZD9WnmFViMrvfvMXLGVFQfAAcC060eGFSRJ1 +ZQ9UVQIDAQABoy0wKzAbBgNVHREEFDASgRBzbWhhdW5jaEBtYWMuY29tMAwGA1UdEwEB/wQCMAAw +DQYJKoZIhvcNAQEEBQADgYEAQMrg1n2pXVWteP7BBj+Pk3UfYtbuHb42uHcLJjfjnRlH7AxnSwrd +L3HED205w3Cq8T7tzVxIjRRLO/ljq0GedSCFBky7eYo1PrXhztGHCTSBhsiWdiyLWxKlOxGAwJc/ +lMMnwqLOdrQcoF/YgbjeaUFOQbUh94w9VDNpWZYCZwcwggM/MIICqKADAgECAgENMA0GCSqGSIb3 +DQEBBQUAMIHRMQswCQYDVQQGEwJaQTEVMBMGA1UECBMMV2VzdGVybiBDYXBlMRIwEAYDVQQHEwlD +YXBlIFRvd24xGjAYBgNVBAoTEVRoYXd0ZSBDb25zdWx0aW5nMSgwJgYDVQQLEx9DZXJ0aWZpY2F0 +aW9uIFNlcnZpY2VzIERpdmlzaW9uMSQwIgYDVQQDExtUaGF3dGUgUGVyc29uYWwgRnJlZW1haWwg +Q0ExKzApBgkqhkiG9w0BCQEWHHBlcnNvbmFsLWZyZWVtYWlsQHRoYXd0ZS5jb20wHhcNMDMwNzE3 +MDAwMDAwWhcNMTMwNzE2MjM1OTU5WjBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENv +bnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElz +c3VpbmcgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMSmPFVzVftOucqZWh5owHUEcJ3f +6f+jHuy9zfVb8hp2vX8MOmHyv1HOAdTlUAow1wJjWiyJFXCO3cnwK4Vaqj9xVsuvPAsH5/EfkTYk +KhPPK9Xzgnc9A74r/rsYPge/QIACZNenprufZdHFKlSFD0gEf6e20TxhBEAeZBlyYLf7AgMBAAGj +gZQwgZEwEgYDVR0TAQH/BAgwBgEB/wIBADBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLnRo +YXd0ZS5jb20vVGhhd3RlUGVyc29uYWxGcmVlbWFpbENBLmNybDALBgNVHQ8EBAMCAQYwKQYDVR0R +BCIwIKQeMBwxGjAYBgNVBAMTEVByaXZhdGVMYWJlbDItMTM4MA0GCSqGSIb3DQEBBQUAA4GBAEiM +0VCD6gsuzA2jZqxnD3+vrL7CF6FDlpSdf0whuPg2H6otnzYvwPQcUCCTcDz9reFhYsPZOhl+hLGZ +GwDFGguCdJ4lUJRix9sncVcljd2pnDmOjCBPZV+V2vf3h9bGCE6u9uo05RAaWzVNd+NWIXiC3CEZ +Nd4ksdMdRv9dX2VPMYIC5zCCAuMCAQEwaTBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3Rl +IENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWls +IElzc3VpbmcgQ0ECAw5c+TAJBgUrDgMCGgUAoIIBUzAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcB +MBwGCSqGSIb3DQEJBTEPFw0wNTA1MDgxODE3NDZaMCMGCSqGSIb3DQEJBDEWBBQSkG9j6+hB0pKp +fV9tCi/iP59sNTB4BgkrBgEEAYI3EAQxazBpMGIxCzAJBgNVBAYTAlpBMSUwIwYDVQQKExxUaGF3 +dGUgQ29uc3VsdGluZyAoUHR5KSBMdGQuMSwwKgYDVQQDEyNUaGF3dGUgUGVyc29uYWwgRnJlZW1h +aWwgSXNzdWluZyBDQQIDDlz5MHoGCyqGSIb3DQEJEAILMWugaTBiMQswCQYDVQQGEwJaQTElMCMG +A1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNv +bmFsIEZyZWVtYWlsIElzc3VpbmcgQ0ECAw5c+TANBgkqhkiG9w0BAQEFAASCAQAm1GeF7dWfMvrW +8yMPjkhE+R8D1DsiCoWSCp+5gAQm7lcK7V3KrZh5howfpI3TmCZUbbaMxOH+7aKRKpFemxoBY5Q8 +rnCkbpg/++/+MI01T69hF/rgMmrGcrv2fIYy8EaARLG0xUVFSZHSP+NQSYz0TTmh4cAESHMzY3JA +nHOoUkuPyl8RXrimY1zn0lceMXlweZRouiPGuPNl1hQKw8P+GhOC5oLlM71UtStnrlk3P9gqX5v7 +Tj7Hx057oVfY8FMevjxGwU3EK5TczHezHbWWgTyum9l2ZQbUQsDJxSniD3BM46C1VcbDLPaotAZ0 +fTYLZizQfm5hcWEbfYVzkSzLAAAAAAAA +------=_Part_5028_7368284.1115579351471-- + diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email3 b/vendor/rails/actionmailer/test/fixtures/raw_email3 new file mode 100644 index 00000000..771a9635 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email3 @@ -0,0 +1,70 @@ +From xxxx@xxxx.com Tue May 10 11:28:07 2005 +Return-Path: +X-Original-To: xxxx@xxxx.com +Delivered-To: xxxx@xxxx.com +Received: from localhost (localhost [127.0.0.1]) + by xxx.xxxxx.com (Postfix) with ESMTP id 50FD3A96F + for ; Tue, 10 May 2005 17:26:50 +0000 (GMT) +Received: from xxx.xxxxx.com ([127.0.0.1]) + by localhost (xxx.xxxxx.com [127.0.0.1]) (amavisd-new, port 10024) + with LMTP id 70060-03 for ; + Tue, 10 May 2005 17:26:49 +0000 (GMT) +Received: from xxx.xxxxx.com (xxx.xxxxx.com [69.36.39.150]) + by xxx.xxxxx.com (Postfix) with ESMTP id 8B957A94B + for ; Tue, 10 May 2005 17:26:48 +0000 (GMT) +Received: from xxx.xxxxx.com (xxx.xxxxx.com [64.233.184.203]) + by xxx.xxxxx.com (Postfix) with ESMTP id 9972514824C + for ; Tue, 10 May 2005 12:26:40 -0500 (CDT) +Received: by xxx.xxxxx.com with SMTP id 68so1694448wri + for ; Tue, 10 May 2005 10:26:40 -0700 (PDT) +DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws; + s=beta; d=xxxxx.com; + h=received:message-id:date:from:reply-to:to:subject:mime-version:content-type; + b=g8ZO5ttS6GPEMAz9WxrRk9+9IXBUfQIYsZLL6T88+ECbsXqGIgfGtzJJFn6o9CE3/HMrrIGkN5AisxVFTGXWxWci5YA/7PTVWwPOhJff5BRYQDVNgRKqMl/SMttNrrRElsGJjnD1UyQ/5kQmcBxq2PuZI5Zc47u6CILcuoBcM+A= +Received: by 10.54.96.19 with SMTP id t19mr621017wrb; + Tue, 10 May 2005 10:26:39 -0700 (PDT) +Received: by 10.54.110.5 with HTTP; Tue, 10 May 2005 10:26:39 -0700 (PDT) +Message-ID: +Date: Tue, 10 May 2005 11:26:39 -0600 +From: Test Tester +Reply-To: Test Tester +To: xxxx@xxxx.com, xxxx@xxxx.com +Subject: Another PDF +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_Part_2192_32400445.1115745999735" +X-Virus-Scanned: amavisd-new at textdrive.com + +------=_Part_2192_32400445.1115745999735 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +Just attaching another PDF, here, to see what the message looks like, +and to see if I can figure out what is going wrong here. + +------=_Part_2192_32400445.1115745999735 +Content-Type: application/pdf; name="broken.pdf" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="broken.pdf" + +JVBERi0xLjQNCiXk9tzfDQoxIDAgb2JqDQo8PCAvTGVuZ3RoIDIgMCBSDQogICAvRmlsdGVyIC9G +bGF0ZURlY29kZQ0KPj4NCnN0cmVhbQ0KeJy9Wt2KJbkNvm/od6jrhZxYln9hWEh2p+8HBvICySaE +ycLuTV4/1ifJ9qnq09NpSBimu76yLUuy/qzqcPz7+em3Ixx/CDc6CsXxs3b5+fvfjr/8cPz6/BRu +rbfAx/n3739/fuJylJ5u5fjX81OuDr4deK4Bz3z/aDP+8fz0yw8g0Ofq7ktr1Mn+u28rvhy/jVeD +QSa+9YNKHP/pxjvDNfVAx/m3MFz54FhvTbaseaxiDoN2LeMVMw+yA7RbHSCDzxZuaYB2E1Yay7QU +x89vz0+tyFDKMlAHK5yqLmnjF+c4RjEiQIUeKwblXMe+AsZjN1J5yGQL5DHpDHksurM81rF6PKab +gK6zAarIDzIiUY23rJsN9iorAE816aIu6lsgAdQFsuhhkHOUFgVjp2GjMqSewITXNQ27jrMeamkg +1rPI3iLWG2CIaSBB+V1245YVRICGbbpYKHc2USFDl6M09acQVQYhlwIrkBNLISvXhGlF1wi5FHCw +wxZkoGNJlVeJCEsqKA+3YAV5AMb6KkeaqEJQmFKKQU8T1pRi2ihE1Y4CDrqoYFFXYjJJOatsyzuI +8SIlykuxKTMibWK8H1PgEvqYgs4GmQSrEjJAalgGirIhik+p4ZQN9E3ETFPAHE1b8pp1l/0Rc1gl +fQs0ABWvyoZZzU8VnPXwVVcO9BEsyjEJaO6eBoZRyKGlrKoYoOygA8BGIzgwN3RQ15ouigG5idZQ +fx2U4Db2CqiLO0WHAZoylGiCAqhniNQjFjQPSkmjwfNTgQ6M1Ih+eWo36wFmjIxDJZiGUBiWsAyR +xX3EekGOizkGI96Ol9zVZTAivikURhRsHh2E3JhWMpSTZCnnonrLhMCodgrNcgo4uyJUJc6qnVss +nrGd1Ptr0YwisCOYyIbUwVjV4xBUNLbguSO2YHujonAMJkMdSI7bIw91Akq2AUlMUWGFTMAOamjU +OvZQCxIkY2pCpMFo/IwLdVLHs6nddwTRrgoVbvLU9eB0G4EMndV0TNoxHbt3JBWwK6hhv3iHfDtF +yokB302IpEBTnWICde4uYc/1khDbSIkQopO6lcqamGBu1OSE3N5IPSsZX00CkSHRiiyx6HQIShsS +HSVNswdVsaOUSAWq9aYhDtGDaoG5a3lBGkYt/lFlBFt1UqrYnzVtUpUQnLiZeouKgf1KhRBViRRk +ExepJCzTwEmFDalIRbLEGtw0gfpESOpIAF/NnpPzcVCG86s0g2DuSyd41uhNGbEgaSrWEXORErbw +------=_Part_2192_32400445.1115745999735-- + diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email4 b/vendor/rails/actionmailer/test/fixtures/raw_email4 new file mode 100644 index 00000000..639ad40e --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email4 @@ -0,0 +1,59 @@ +Return-Path: +Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id 6AAEE3B4D23 for ; Sun, 8 May 2005 12:30:23 -0500 +Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id j48HUC213279 for ; Sun, 8 May 2005 12:30:13 -0500 +Received: from conversion-xxx.xxxx.xxx.net by xxx.xxxx.xxx id <0IG600901LQ64I@xxx.xxxx.xxx> for ; Sun, 8 May 2005 12:30:12 -0500 +Received: from agw1 by xxx.xxxx.xxx with ESMTP id <0IG600JFYLYCAxxx@xxxx.xxx> for ; Sun, 8 May 2005 12:30:12 -0500 +Date: Sun, 8 May 2005 12:30:08 -0500 +From: xxx@xxxx.xxx +To: xxx@xxxx.xxx +Message-Id: <7864245.1115573412626.JavaMxxx@xxxx.xxx> +Subject: Filth +Mime-Version: 1.0 +Content-Type: multipart/mixed; boundary=mimepart_427e4cb4ca329_133ae40413c81ef +X-Mms-Priority: 1 +X-Mms-Transaction-Id: 3198421808-0 +X-Mms-Message-Type: 0 +X-Mms-Sender-Visibility: 1 +X-Mms-Read-Reply: 1 +X-Original-To: xxx@xxxx.xxx +X-Mms-Message-Class: 0 +X-Mms-Delivery-Report: 0 +X-Mms-Mms-Version: 16 +Delivered-To: xxx@xxxx.xxx +X-Nokia-Ag-Version: 2.0 + +This is a multi-part message in MIME format. + +--mimepart_427e4cb4ca329_133ae40413c81ef +Content-Type: multipart/mixed; boundary=mimepart_427e4cb4cbd97_133ae40413c8217 + + + +--mimepart_427e4cb4cbd97_133ae40413c8217 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline +Content-Location: text.txt + +Some text + +--mimepart_427e4cb4cbd97_133ae40413c8217-- + +--mimepart_427e4cb4ca329_133ae40413c81ef +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +-- +This Orange Multi Media Message was sent wirefree from an Orange +MMS phone. If you would like to reply, please text or phone the +sender directly by using the phone number listed in the sender's +address. To learn more about Orange's Multi Media Messaging +Service, find us on the Web at xxx.xxxx.xxx.uk/mms + + +--mimepart_427e4cb4ca329_133ae40413c81ef + + +--mimepart_427e4cb4ca329_133ae40413c81ef- + diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email5 b/vendor/rails/actionmailer/test/fixtures/raw_email5 new file mode 100644 index 00000000..151c6314 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email5 @@ -0,0 +1,19 @@ +Return-Path: +Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for ; Tue, 10 May 2005 15:27:05 -0500 +Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for ; Tue, 10 May 2005 15:27:04 -0500 +Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for ; Tue, 10 May 2005 15:27:03 -0500 +Date: Tue, 10 May 2005 15:27:03 -0500 +From: xxx@xxxx.xxx +Sender: xxx@xxxx.xxx +To: xxxxxxxxxxx@xxxx.xxxx.xxx +Message-Id: +X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx +Delivered-To: xxx@xxxx.xxx +Importance: normal + +Test test. Hi. Waving. m + +---------------------------------------------------------------- +Sent via Bell Mobility's Text Messaging service. +Envoyé par le service de messagerie texte de Bell Mobilité. +---------------------------------------------------------------- diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email6 b/vendor/rails/actionmailer/test/fixtures/raw_email6 new file mode 100644 index 00000000..93289c4f --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email6 @@ -0,0 +1,20 @@ +Return-Path: +Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for ; Tue, 10 May 2005 15:27:05 -0500 +Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for ; Tue, 10 May 2005 15:27:04 -0500 +Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for ; Tue, 10 May 2005 15:27:03 -0500 +Date: Tue, 10 May 2005 15:27:03 -0500 +From: xxx@xxxx.xxx +Sender: xxx@xxxx.xxx +To: xxxxxxxxxxx@xxxx.xxxx.xxx +Message-Id: +X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx +Delivered-To: xxx@xxxx.xxx +Importance: normal +Content-Type: text/plain; charset=us-ascii + +Test test. Hi. Waving. m + +---------------------------------------------------------------- +Sent via Bell Mobility's Text Messaging service. +Envoyé par le service de messagerie texte de Bell Mobilité. +---------------------------------------------------------------- diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email7 b/vendor/rails/actionmailer/test/fixtures/raw_email7 new file mode 100644 index 00000000..da64ada8 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email7 @@ -0,0 +1,66 @@ +Mime-Version: 1.0 (Apple Message framework v730) +Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151 +Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com> +From: foo@example.com +Subject: testing +Date: Mon, 6 Jun 2005 22:21:22 +0200 +To: blah@example.com + + +--Apple-Mail-13-196941151 +Content-Type: multipart/mixed; + boundary=Apple-Mail-12-196940926 + + +--Apple-Mail-12-196940926 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=ISO-8859-1; + delsp=yes; + format=flowed + +This is the first part. + +--Apple-Mail-12-196940926 +Content-Transfer-Encoding: 7bit +Content-Type: text/x-ruby-script; + x-unix-mode=0666; + name="test.rb" +Content-Disposition: attachment; + filename=test.rb + +puts "testing, testing" + +--Apple-Mail-12-196940926 +Content-Transfer-Encoding: base64 +Content-Type: application/pdf; + x-unix-mode=0666; + name="test.pdf" +Content-Disposition: inline; + filename=test.pdf + +YmxhaCBibGFoIGJsYWg= + +--Apple-Mail-12-196940926 +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; + charset=US-ASCII; + format=flowed + + + +--Apple-Mail-12-196940926-- + +--Apple-Mail-13-196941151 +Content-Transfer-Encoding: base64 +Content-Type: application/pkcs7-signature; + name=smime.p7s +Content-Disposition: attachment; + filename=smime.p7s + +jamisSqGSIb3DQEHAqCAMIjamisxCzAJBgUrDgMCGgUAMIAGCSqGSjamisEHAQAAoIIFSjCCBUYw +ggQujamisQICBD++ukQwDQYJKojamisNAQEFBQAwMTELMAkGA1UEBhMCRjamisAKBgNVBAoTA1RE +QzEUMBIGjamisxMLVERDIE9DRVMgQ0jamisNMDQwMjI5MTE1OTAxWhcNMDYwMjamisIyOTAxWjCB +gDELMAkGA1UEjamisEsxKTAnBgNVBAoTIEjamisuIG9yZ2FuaXNhdG9yaXNrIHRpbjamisRuaW5= + +--Apple-Mail-13-196941151-- diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email8 b/vendor/rails/actionmailer/test/fixtures/raw_email8 new file mode 100644 index 00000000..2382dfdf --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email8 @@ -0,0 +1,47 @@ +From xxxxxxxxx.xxxxxxx@gmail.com Sun May 8 19:07:09 2005 +Return-Path: +Message-ID: +Date: Sun, 8 May 2005 14:09:11 -0500 +From: xxxxxxxxx xxxxxxx +Reply-To: xxxxxxxxx xxxxxxx +To: xxxxx xxxx +Subject: Fwd: Signed email causes file attachments +In-Reply-To: +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_Part_5028_7368284.1115579351471" +References: + +------=_Part_5028_7368284.1115579351471 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +We should not include these files or vcards as attachments. + +---------- Forwarded message ---------- +From: xxxxx xxxxxx +Date: May 8, 2005 1:17 PM +Subject: Signed email causes file attachments +To: xxxxxxx@xxxxxxxxxx.com + + +Hi, + +Test attachments oddly encoded with japanese charset. + + +------=_Part_5028_7368284.1115579351471 +Content-Type: application/octet-stream; name*=iso-2022-jp'ja'01%20Quien%20Te%20Dij%8aat.%20Pitbull.mp3 +Content-Transfer-Encoding: base64 +Content-Disposition: attachment + +MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGFDCCAs0w +ggI2oAMCAQICAw5c+TANBgkqhkiG9w0BAQQFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhh +d3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVt +YWlsIElzc3VpbmcgQ0EwHhcNMDUwMzI5MDkzOTEwWhcNMDYwMzI5MDkzOTEwWjBCMR8wHQYDVQQD +ExZUaGF3dGUgRnJlZW1haWwgTWVtYmVyMR8wHQYJKoZIhvcNAQkBFhBzbWhhdW5jaEBtYWMuY29t +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn90dPsYS3LjfMY211OSYrDQLzwNYPlAL +7+/0XA+kdy8/rRnyEHFGwhNCDmg0B6pxC7z3xxJD/8GfCd+IYUUNUQV5m9MkxfP9pTVXZVIYLaBw +------=_Part_5028_7368284.1115579351471-- + diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email9 b/vendor/rails/actionmailer/test/fixtures/raw_email9 new file mode 100644 index 00000000..8b9b1eaa --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email9 @@ -0,0 +1,28 @@ +Received: from xxx.xxx.xxx ([xxx.xxx.xxx.xxx] verified) + by xxx.com (CommuniGate Pro SMTP 4.2.8) + with SMTP id 2532598 for xxx@xxx.com; Wed, 23 Feb 2005 17:51:49 -0500 +Received-SPF: softfail + receiver=xxx.com; client-ip=xxx.xxx.xxx.xxx; envelope-from=xxx@xxx.xxx +quite Delivered-To: xxx@xxx.xxx +Received: by xxx.xxx.xxx (Wostfix, from userid xxx) + id 0F87F333; Wed, 23 Feb 2005 16:16:17 -0600 +Date: Wed, 23 Feb 2005 18:20:17 -0400 +From: "xxx xxx" +Message-ID: <4D6AA7EB.6490534@xxx.xxx> +To: xxx@xxx.com +Subject: Stop adware/spyware once and for all. +X-Scanned-By: MIMEDefang 2.11 (www dot roaringpenguin dot com slash mimedefang) + +You are infected with: +Ad Ware and Spy Ware + +Get your free scan and removal download now, +before it gets any worse. + +http://xxx.xxx.info?aid=3D13&?stat=3D4327kdzt + + + + +no more? (you will still be infected) +http://xxx.xxx.info/discon/?xxx@xxx.com diff --git a/vendor/rails/actionmailer/test/fixtures/templates/signed_up.rhtml b/vendor/rails/actionmailer/test/fixtures/templates/signed_up.rhtml new file mode 100644 index 00000000..a85d5fa4 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/templates/signed_up.rhtml @@ -0,0 +1,3 @@ +Hello there, + +Mr. <%= @recipient %> \ No newline at end of file diff --git a/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.ignored.rhtml b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.ignored.rhtml new file mode 100644 index 00000000..6940419d --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.ignored.rhtml @@ -0,0 +1 @@ +Ignored when searching for implicitly multipart parts. diff --git a/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.html.rhtml b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.html.rhtml new file mode 100644 index 00000000..946d99ed --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.html.rhtml @@ -0,0 +1,10 @@ + + + HTML formatted message to <%= @recipient %>. + + + + + HTML formatted message to <%= @recipient %>. + + diff --git a/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.plain.rhtml b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.plain.rhtml new file mode 100644 index 00000000..a6c8d54c --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.plain.rhtml @@ -0,0 +1,2 @@ +Plain text to <%= @recipient %>. +Plain text to <%= @recipient %>. diff --git a/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.yaml.rhtml b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.yaml.rhtml new file mode 100644 index 00000000..c14348c7 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.yaml.rhtml @@ -0,0 +1 @@ +yaml to: <%= @recipient %> \ No newline at end of file diff --git a/vendor/rails/actionmailer/test/fixtures/test_mailer/signed_up.rhtml b/vendor/rails/actionmailer/test/fixtures/test_mailer/signed_up.rhtml new file mode 100644 index 00000000..a85d5fa4 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/test_mailer/signed_up.rhtml @@ -0,0 +1,3 @@ +Hello there, + +Mr. <%= @recipient %> \ No newline at end of file diff --git a/vendor/rails/actionmailer/test/mail_helper_test.rb b/vendor/rails/actionmailer/test/mail_helper_test.rb new file mode 100644 index 00000000..bf5bf7f3 --- /dev/null +++ b/vendor/rails/actionmailer/test/mail_helper_test.rb @@ -0,0 +1,97 @@ +$:.unshift(File.dirname(__FILE__) + "/../lib/") +$:.unshift File.dirname(__FILE__) + "/fixtures/helpers" + +require 'test/unit' +require 'action_mailer' + +module MailerHelper + def person_name + "Mr. Joe Person" + end +end + +class HelperMailer < ActionMailer::Base + helper MailerHelper + helper :test + + def use_helper(recipient) + recipients recipient + subject "using helpers" + from "tester@example.com" + end + + def use_test_helper(recipient) + recipients recipient + subject "using helpers" + from "tester@example.com" + self.body = { :text => "emphasize me!" } + end + + def use_mail_helper(recipient) + recipients recipient + subject "using mailing helpers" + from "tester@example.com" + self.body = { :text => + "But soft! What light through yonder window breaks? It is the east, " + + "and Juliet is the sun. Arise, fair sun, and kill the envious moon, " + + "which is sick and pale with grief that thou, her maid, art far more " + + "fair than she. Be not her maid, for she is envious! Her vestal " + + "livery is but sick and green, and none but fools do wear it. Cast " + + "it off!" + } + end + + def use_helper_method(recipient) + recipients recipient + subject "using helpers" + from "tester@example.com" + self.body = { :text => "emphasize me!" } + end + + private + + def name_of_the_mailer_class + self.class.name + end + helper_method :name_of_the_mailer_class +end + +HelperMailer.template_root = File.dirname(__FILE__) + "/fixtures" + +class MailerHelperTest < Test::Unit::TestCase + def new_mail( charset="utf-8" ) + mail = TMail::Mail.new + mail.set_content_type "text", "plain", { "charset" => charset } if charset + mail + end + + def setup + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries = [] + + @recipient = 'test@localhost' + end + + def test_use_helper + mail = HelperMailer.create_use_helper(@recipient) + assert_match %r{Mr. Joe Person}, mail.encoded + end + + def test_use_test_helper + mail = HelperMailer.create_use_test_helper(@recipient) + assert_match %r{emphasize me!}, mail.encoded + end + + def test_use_helper_method + mail = HelperMailer.create_use_helper_method(@recipient) + assert_match %r{HelperMailer}, mail.encoded + end + + def test_use_mail_helper + mail = HelperMailer.create_use_mail_helper(@recipient) + assert_match %r{ But soft!}, mail.encoded + assert_match %r{east, and\n Juliet}, mail.encoded + end +end + diff --git a/vendor/rails/actionmailer/test/mail_render_test.rb b/vendor/rails/actionmailer/test/mail_render_test.rb new file mode 100644 index 00000000..d5819652 --- /dev/null +++ b/vendor/rails/actionmailer/test/mail_render_test.rb @@ -0,0 +1,48 @@ +$:.unshift(File.dirname(__FILE__) + "/../lib/") + +require 'test/unit' +require 'action_mailer' + +class RenderMailer < ActionMailer::Base + def inline_template(recipient) + recipients recipient + subject "using helpers" + from "tester@example.com" + body render(:inline => "Hello, <%= @world %>", :body => { :world => "Earth" }) + end + + def file_template(recipient) + recipients recipient + subject "using helpers" + from "tester@example.com" + body render(:file => "signed_up", :body => { :recipient => recipient }) + end + + def initialize_defaults(method_name) + super + mailer_name "test_mailer" + end +end + +RenderMailer.template_root = File.dirname(__FILE__) + "/fixtures" + +class RenderHelperTest < Test::Unit::TestCase + def setup + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries = [] + + @recipient = 'test@localhost' + end + + def test_inline_template + mail = RenderMailer.create_inline_template(@recipient) + assert_equal "Hello, Earth", mail.body.strip + end + + def test_file_template + mail = RenderMailer.create_file_template(@recipient) + assert_equal "Hello there, \n\nMr. test@localhost", mail.body.strip + end +end + diff --git a/vendor/rails/actionmailer/test/mail_service_test.rb b/vendor/rails/actionmailer/test/mail_service_test.rb new file mode 100755 index 00000000..5fdf4074 --- /dev/null +++ b/vendor/rails/actionmailer/test/mail_service_test.rb @@ -0,0 +1,832 @@ +$:.unshift(File.dirname(__FILE__) + "/../lib/") + +require 'test/unit' +require 'action_mailer' + +class MockSMTP + def self.deliveries + @@deliveries + end + + def initialize + @@deliveries = [] + end + + def sendmail(mail, from, to) + @@deliveries << [mail, from, to] + end +end + +class Net::SMTP + def self.start(*args) + yield MockSMTP.new + end +end + +class FunkyPathMailer < ActionMailer::Base + self.template_root = "#{File.dirname(__FILE__)}/fixtures/path.with.dots" + + def multipart_with_template_path_with_dots(recipient) + recipients recipient + subject "Have a lovely picture" + from "Chad Fowler " + attachment :content_type => "image/jpeg", + :body => "not really a jpeg, we're only testing, after all" + end + + def template_path + "#{File.dirname(__FILE__)}/fixtures/path.with.dots" + end +end + +class TestMailer < ActionMailer::Base + + def signed_up(recipient) + @recipients = recipient + @subject = "[Signed up] Welcome #{recipient}" + @from = "system@loudthinking.com" + @sent_on = Time.local(2004, 12, 12) + @body["recipient"] = recipient + end + + def cancelled_account(recipient) + self.recipients = recipient + self.subject = "[Cancelled] Goodbye #{recipient}" + self.from = "system@loudthinking.com" + self.sent_on = Time.local(2004, 12, 12) + self.body = "Goodbye, Mr. #{recipient}" + end + + def cc_bcc(recipient) + recipients recipient + subject "testing bcc/cc" + from "system@loudthinking.com" + sent_on Time.local(2004, 12, 12) + cc "nobody@loudthinking.com" + bcc "root@loudthinking.com" + body "Nothing to see here." + end + + def iso_charset(recipient) + @recipients = recipient + @subject = "testing isø charsets" + @from = "system@loudthinking.com" + @sent_on = Time.local 2004, 12, 12 + @cc = "nobody@loudthinking.com" + @bcc = "root@loudthinking.com" + @body = "Nothing to see here." + @charset = "iso-8859-1" + end + + def unencoded_subject(recipient) + @recipients = recipient + @subject = "testing unencoded subject" + @from = "system@loudthinking.com" + @sent_on = Time.local 2004, 12, 12 + @cc = "nobody@loudthinking.com" + @bcc = "root@loudthinking.com" + @body = "Nothing to see here." + end + + def extended_headers(recipient) + @recipients = recipient + @subject = "testing extended headers" + @from = "Grytøyr " + @sent_on = Time.local 2004, 12, 12 + @cc = "Grytøyr " + @bcc = "Grytøyr " + @body = "Nothing to see here." + @charset = "iso-8859-1" + end + + def utf8_body(recipient) + @recipients = recipient + @subject = "testing utf-8 body" + @from = "Foo áëô îü " + @sent_on = Time.local 2004, 12, 12 + @cc = "Foo áëô îü " + @bcc = "Foo áëô îü " + @body = "åœö blah" + @charset = "utf-8" + end + + def multipart_with_mime_version(recipient) + recipients recipient + subject "multipart with mime_version" + from "test@example.com" + sent_on Time.local(2004, 12, 12) + mime_version "1.1" + content_type "multipart/alternative" + + part "text/plain" do |p| + p.body = "blah" + end + + part "text/html" do |p| + p.body = "blah" + end + end + + def multipart_with_utf8_subject(recipient) + recipients recipient + subject "Foo áëô îü" + from "test@example.com" + charset "utf-8" + + part "text/plain" do |p| + p.body = "blah" + end + + part "text/html" do |p| + p.body = "blah" + end + end + + def explicitly_multipart_example(recipient, ct=nil) + recipients recipient + subject "multipart example" + from "test@example.com" + sent_on Time.local(2004, 12, 12) + body "plain text default" + content_type ct if ct + + part "text/html" do |p| + p.charset = "iso-8859-1" + p.body = "blah" + end + + attachment :content_type => "image/jpeg", :filename => "foo.jpg", + :body => "123456789" + end + + def implicitly_multipart_example(recipient, cs = nil, order = nil) + @recipients = recipient + @subject = "multipart example" + @from = "test@example.com" + @sent_on = Time.local 2004, 12, 12 + @body = { "recipient" => recipient } + @charset = cs if cs + @implicit_parts_order = order if order + end + + def implicitly_multipart_with_utf8 + recipients "no.one@nowhere.test" + subject "Foo áëô îü" + from "some.one@somewhere.test" + template "implicitly_multipart_example" + body ({ "recipient" => "no.one@nowhere.test" }) + end + + def html_mail(recipient) + recipients recipient + subject "html mail" + from "test@example.com" + body "Emphasize this" + content_type "text/html" + end + + def html_mail_with_underscores(recipient) + subject "html mail with underscores" + body %{_Google} + end + + def custom_template(recipient) + recipients recipient + subject "[Signed up] Welcome #{recipient}" + from "system@loudthinking.com" + sent_on Time.local(2004, 12, 12) + template "signed_up" + + body["recipient"] = recipient + end + + def various_newlines(recipient) + recipients recipient + subject "various newlines" + from "test@example.com" + body "line #1\nline #2\rline #3\r\nline #4\r\r" + + "line #5\n\nline#6\r\n\r\nline #7" + end + + def various_newlines_multipart(recipient) + recipients recipient + subject "various newlines multipart" + from "test@example.com" + content_type "multipart/alternative" + part :content_type => "text/plain", :body => "line #1\nline #2\rline #3\r\nline #4\r\r" + part :content_type => "text/html", :body => "

    line #1

    \n

    line #2

    \r

    line #3

    \r\n

    line #4

    \r\r" + end + + def nested_multipart(recipient) + recipients recipient + subject "nested multipart" + from "test@example.com" + content_type "multipart/mixed" + part :content_type => "multipart/alternative", :content_disposition => "inline" do |p| + p.part :content_type => "text/plain", :body => "test text\nline #2" + p.part :content_type => "text/html", :body => "test HTML
    \nline #2" + end + attachment :content_type => "application/octet-stream",:filename => "test.txt", :body => "test abcdefghijklmnopqstuvwxyz" + end + + def attachment_with_custom_header(recipient) + recipients recipient + subject "custom header in attachment" + from "test@example.com" + content_type "multipart/related" + part :content_type => "text/html", :body => 'yo' + attachment :content_type => "image/jpeg",:filename => "test.jpeg", :body => "i am not a real picture", :headers => { 'Content-ID' => '' } + end + + def unnamed_attachment(recipient) + recipients recipient + subject "nested multipart" + from "test@example.com" + content_type "multipart/mixed" + part :content_type => "text/plain", :body => "hullo" + attachment :content_type => "application/octet-stream", :body => "test abcdefghijklmnopqstuvwxyz" + end + + def headers_with_nonalpha_chars(recipient) + recipients recipient + subject "nonalpha chars" + from "One: Two " + cc "Three: Four " + bcc "Five: Six " + body "testing" + end + + def custom_content_type_attributes + recipients "no.one@nowhere.test" + subject "custom content types" + from "some.one@somewhere.test" + content_type "text/plain; format=flowed" + body "testing" + end + + class < charset } + end + mail + end + + def setup + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries = [] + + @recipient = 'test@localhost' + end + + def test_nested_parts + created = nil + assert_nothing_raised { created = TestMailer.create_nested_multipart(@recipient)} + assert_equal 2,created.parts.size + assert_equal 2,created.parts.first.parts.size + + assert_equal "multipart/mixed", created.content_type + assert_equal "multipart/alternative", created.parts.first.content_type + assert_equal "text/plain", created.parts.first.parts.first.content_type + assert_equal "text/html", created.parts.first.parts[1].content_type + assert_equal "application/octet-stream", created.parts[1].content_type + end + + def test_attachment_with_custom_header + created = nil + assert_nothing_raised { created = TestMailer.create_attachment_with_custom_header(@recipient)} + assert_equal "", created.parts[1].header['content-id'].to_s + end + + def test_signed_up + expected = new_mail + expected.to = @recipient + expected.subject = "[Signed up] Welcome #{@recipient}" + expected.body = "Hello there, \n\nMr. #{@recipient}" + expected.from = "system@loudthinking.com" + expected.date = Time.local(2004, 12, 12) + expected.mime_version = nil + + created = nil + assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) } + assert_not_nil created + assert_equal expected.encoded, created.encoded + + assert_nothing_raised { TestMailer.deliver_signed_up(@recipient) } + assert_not_nil ActionMailer::Base.deliveries.first + assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded + end + + def test_custom_template + expected = new_mail + expected.to = @recipient + expected.subject = "[Signed up] Welcome #{@recipient}" + expected.body = "Hello there, \n\nMr. #{@recipient}" + expected.from = "system@loudthinking.com" + expected.date = Time.local(2004, 12, 12) + + created = nil + assert_nothing_raised { created = TestMailer.create_custom_template(@recipient) } + assert_not_nil created + assert_equal expected.encoded, created.encoded + end + + def test_cancelled_account + expected = new_mail + expected.to = @recipient + expected.subject = "[Cancelled] Goodbye #{@recipient}" + expected.body = "Goodbye, Mr. #{@recipient}" + expected.from = "system@loudthinking.com" + expected.date = Time.local(2004, 12, 12) + + created = nil + assert_nothing_raised { created = TestMailer.create_cancelled_account(@recipient) } + assert_not_nil created + assert_equal expected.encoded, created.encoded + + assert_nothing_raised { TestMailer.deliver_cancelled_account(@recipient) } + assert_not_nil ActionMailer::Base.deliveries.first + assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded + end + + def test_cc_bcc + expected = new_mail + expected.to = @recipient + expected.subject = "testing bcc/cc" + expected.body = "Nothing to see here." + expected.from = "system@loudthinking.com" + expected.cc = "nobody@loudthinking.com" + expected.bcc = "root@loudthinking.com" + expected.date = Time.local 2004, 12, 12 + + created = nil + assert_nothing_raised do + created = TestMailer.create_cc_bcc @recipient + end + assert_not_nil created + assert_equal expected.encoded, created.encoded + + assert_nothing_raised do + TestMailer.deliver_cc_bcc @recipient + end + + assert_not_nil ActionMailer::Base.deliveries.first + assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded + end + + def test_iso_charset + expected = new_mail( "iso-8859-1" ) + expected.to = @recipient + expected.subject = encode "testing isø charsets", "iso-8859-1" + expected.body = "Nothing to see here." + expected.from = "system@loudthinking.com" + expected.cc = "nobody@loudthinking.com" + expected.bcc = "root@loudthinking.com" + expected.date = Time.local 2004, 12, 12 + + created = nil + assert_nothing_raised do + created = TestMailer.create_iso_charset @recipient + end + assert_not_nil created + assert_equal expected.encoded, created.encoded + + assert_nothing_raised do + TestMailer.deliver_iso_charset @recipient + end + + assert_not_nil ActionMailer::Base.deliveries.first + assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded + end + + def test_unencoded_subject + expected = new_mail + expected.to = @recipient + expected.subject = "testing unencoded subject" + expected.body = "Nothing to see here." + expected.from = "system@loudthinking.com" + expected.cc = "nobody@loudthinking.com" + expected.bcc = "root@loudthinking.com" + expected.date = Time.local 2004, 12, 12 + + created = nil + assert_nothing_raised do + created = TestMailer.create_unencoded_subject @recipient + end + assert_not_nil created + assert_equal expected.encoded, created.encoded + + assert_nothing_raised do + TestMailer.deliver_unencoded_subject @recipient + end + + assert_not_nil ActionMailer::Base.deliveries.first + assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded + end + + def test_instances_are_nil + assert_nil ActionMailer::Base.new + assert_nil TestMailer.new + end + + def test_deliveries_array + assert_not_nil ActionMailer::Base.deliveries + assert_equal 0, ActionMailer::Base.deliveries.size + TestMailer.deliver_signed_up(@recipient) + assert_equal 1, ActionMailer::Base.deliveries.size + assert_not_nil ActionMailer::Base.deliveries.first + end + + def test_perform_deliveries_flag + ActionMailer::Base.perform_deliveries = false + TestMailer.deliver_signed_up(@recipient) + assert_equal 0, ActionMailer::Base.deliveries.size + ActionMailer::Base.perform_deliveries = true + TestMailer.deliver_signed_up(@recipient) + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_unquote_quoted_printable_subject + msg = <" + + expected = new_mail "iso-8859-1" + expected.to = quote_address_if_necessary @recipient, "iso-8859-1" + expected.subject = "testing extended headers" + expected.body = "Nothing to see here." + expected.from = quote_address_if_necessary "Grytøyr ", "iso-8859-1" + expected.cc = quote_address_if_necessary "Grytøyr ", "iso-8859-1" + expected.bcc = quote_address_if_necessary "Grytøyr ", "iso-8859-1" + expected.date = Time.local 2004, 12, 12 + + created = nil + assert_nothing_raised do + created = TestMailer.create_extended_headers @recipient + end + + assert_not_nil created + assert_equal expected.encoded, created.encoded + + assert_nothing_raised do + TestMailer.deliver_extended_headers @recipient + end + + assert_not_nil ActionMailer::Base.deliveries.first + assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded + end + + def test_utf8_body_is_not_quoted + @recipient = "Foo áëô îü " + expected = new_mail "utf-8" + expected.to = quote_address_if_necessary @recipient, "utf-8" + expected.subject = "testing utf-8 body" + expected.body = "åœö blah" + expected.from = quote_address_if_necessary @recipient, "utf-8" + expected.cc = quote_address_if_necessary @recipient, "utf-8" + expected.bcc = quote_address_if_necessary @recipient, "utf-8" + expected.date = Time.local 2004, 12, 12 + + created = TestMailer.create_utf8_body @recipient + assert_match(/åœö blah/, created.encoded) + end + + def test_multiple_utf8_recipients + @recipient = ["\"Foo áëô îü\" ", "\"Example Recipient\" "] + expected = new_mail "utf-8" + expected.to = quote_address_if_necessary @recipient, "utf-8" + expected.subject = "testing utf-8 body" + expected.body = "åœö blah" + expected.from = quote_address_if_necessary @recipient.first, "utf-8" + expected.cc = quote_address_if_necessary @recipient, "utf-8" + expected.bcc = quote_address_if_necessary @recipient, "utf-8" + expected.date = Time.local 2004, 12, 12 + + created = TestMailer.create_utf8_body @recipient + assert_match(/\nFrom: =\?utf-8\?Q\?Foo_.*?\?= \r/, created.encoded) + assert_match(/\nTo: =\?utf-8\?Q\?Foo_.*?\?= , Example Recipient _Google}, mail.body + end + + def test_various_newlines + mail = TestMailer.create_various_newlines(@recipient) + assert_equal("line #1\nline #2\nline #3\nline #4\n\n" + + "line #5\n\nline#6\n\nline #7", mail.body) + end + + def test_various_newlines_multipart + mail = TestMailer.create_various_newlines_multipart(@recipient) + assert_equal "line #1\nline #2\nline #3\nline #4\n\n", mail.parts[0].body + assert_equal "

    line #1

    \n

    line #2

    \n

    line #3

    \n

    line #4

    \n\n", mail.parts[1].body + end + + def test_headers_removed_on_smtp_delivery + ActionMailer::Base.delivery_method = :smtp + TestMailer.deliver_cc_bcc(@recipient) + assert MockSMTP.deliveries[0][2].include?("root@loudthinking.com") + assert MockSMTP.deliveries[0][2].include?("nobody@loudthinking.com") + assert MockSMTP.deliveries[0][2].include?(@recipient) + assert_match %r{^Cc: nobody@loudthinking.com}, MockSMTP.deliveries[0][0] + assert_match %r{^To: #{@recipient}}, MockSMTP.deliveries[0][0] + assert_no_match %r{^Bcc: root@loudthinking.com}, MockSMTP.deliveries[0][0] + end + + def test_recursive_multipart_processing + fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email7") + mail = TMail::Mail.parse(fixture) + assert_equal "This is the first part.\n\nAttachment: test.rb\nAttachment: test.pdf\n\n\nAttachment: smime.p7s\n", mail.body + end + + def test_decode_encoded_attachment_filename + fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email8") + mail = TMail::Mail.parse(fixture) + attachment = mail.attachments.last + assert_equal "01QuienTeDijat.Pitbull.mp3", attachment.original_filename + end + + def test_wrong_mail_header + fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email9") + assert_raise(TMail::SyntaxError) { TMail::Mail.parse(fixture) } + end + + def test_decode_message_with_unknown_charset + fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email10") + mail = TMail::Mail.parse(fixture) + assert_nothing_raised { mail.body } + end + + def test_decode_message_with_unquoted_atchar_in_header + fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email11") + mail = TMail::Mail.parse(fixture) + assert_not_nil mail.from + end + + def test_empty_header_values_omitted + result = TestMailer.create_unnamed_attachment(@recipient).encoded + assert_match %r{Content-Type: application/octet-stream[^;]}, result + assert_match %r{Content-Disposition: attachment[^;]}, result + end + + def test_headers_with_nonalpha_chars + mail = TestMailer.create_headers_with_nonalpha_chars(@recipient) + assert !mail.from_addrs.empty? + assert !mail.cc_addrs.empty? + assert !mail.bcc_addrs.empty? + assert_match(/:/, mail.from_addrs.to_s) + assert_match(/:/, mail.cc_addrs.to_s) + assert_match(/:/, mail.bcc_addrs.to_s) + end + + def test_deliver_with_mail_object + mail = TestMailer.create_headers_with_nonalpha_chars(@recipient) + assert_nothing_raised { TestMailer.deliver(mail) } + assert_equal 1, TestMailer.deliveries.length + end + + def test_multipart_with_template_path_with_dots + mail = FunkyPathMailer.create_multipart_with_template_path_with_dots(@recipient) + assert_equal 2, mail.parts.length + end + + def test_custom_content_type_attributes + mail = TestMailer.create_custom_content_type_attributes + assert_match %r{format=flowed}, mail['content-type'].to_s + assert_match %r{charset=utf-8}, mail['content-type'].to_s + end +end + +class InheritableTemplateRootTest < Test::Unit::TestCase + def test_attr + expected = "#{File.dirname(__FILE__)}/fixtures/path.with.dots" + assert_equal expected, FunkyPathMailer.template_root + + sub = Class.new(FunkyPathMailer) + sub.template_root = 'test/path' + + assert_equal 'test/path', sub.template_root + assert_equal expected, FunkyPathMailer.template_root + end +end diff --git a/vendor/rails/actionmailer/test/quoting_test.rb b/vendor/rails/actionmailer/test/quoting_test.rb new file mode 100644 index 00000000..6291cd3d --- /dev/null +++ b/vendor/rails/actionmailer/test/quoting_test.rb @@ -0,0 +1,48 @@ +$:.unshift(File.dirname(__FILE__) + "/../lib/") +$:.unshift(File.dirname(__FILE__) + "/../lib/action_mailer/vendor") + +require 'test/unit' +require 'tmail' +require 'tempfile' + +class QuotingTest < Test::Unit::TestCase + def test_quote_multibyte_chars + original = "\303\246 \303\270 and \303\245" + + result = execute_in_sandbox(<<-CODE) + $:.unshift(File.dirname(__FILE__) + "/../lib/") + $KCODE = 'u' + require 'jcode' + require 'action_mailer/quoting' + include ActionMailer::Quoting + quoted_printable(#{original.inspect}, "UTF-8") + CODE + + unquoted = TMail::Unquoter.unquote_and_convert_to(result, nil) + assert_equal unquoted, original + end + + private + + # This whole thing *could* be much simpler, but I don't think Tempfile, + # popen and others exist on all platforms (like Windows). + def execute_in_sandbox(code) + test_name = "#{File.dirname(__FILE__)}/am-quoting-test.#{$$}.rb" + res_name = "#{File.dirname(__FILE__)}/am-quoting-test.#{$$}.out" + + File.open(test_name, "w+") do |file| + file.write(<<-CODE) + block = Proc.new do + #{code} + end + puts block.call + CODE + end + + system("ruby #{test_name} > #{res_name}") or raise "could not run test in sandbox" + File.read(res_name) + ensure + File.delete(test_name) rescue nil + File.delete(res_name) rescue nil + end +end diff --git a/vendor/rails/actionmailer/test/tmail_test.rb b/vendor/rails/actionmailer/test/tmail_test.rb new file mode 100644 index 00000000..3930c7d3 --- /dev/null +++ b/vendor/rails/actionmailer/test/tmail_test.rb @@ -0,0 +1,17 @@ +$:.unshift(File.dirname(__FILE__) + "/../lib/") +$:.unshift File.dirname(__FILE__) + "/fixtures/helpers" + +require 'test/unit' +require 'action_mailer' + +class TMailMailTest < Test::Unit::TestCase + def test_body + m = TMail::Mail.new + expected = 'something_with_underscores' + m.encoding = 'quoted-printable' + quoted_body = [expected].pack('*M') + m.body = quoted_body + assert_equal "something_with_underscores=\n", m.quoted_body + assert_equal expected, m.body + end +end diff --git a/vendor/rails/actionpack/CHANGELOG b/vendor/rails/actionpack/CHANGELOG new file mode 100644 index 00000000..be0d4064 --- /dev/null +++ b/vendor/rails/actionpack/CHANGELOG @@ -0,0 +1,2595 @@ +*1.12.5* (August 10th, 2006) + +* Updated security fix + + +*1.12.4* (August 8th, 2006) + +* Documentation fix: integration test scripts don't require integration_test. #4914 [Frederick Ros ] + +* ActionController::Base Summary documentation rewrite. #4900 [kevin.clark@gmail.com] + +* Fix text_helper.rb documentation rendering. #4725 [Frederick Ros] + +* Fixes bad rendering of JavaScriptMacrosHelper rdoc. #4910 [Frederick Ros] + +* Enhance documentation for setting headers in integration tests. Skip auto HTTP prepending when its already there. #4079 [Rick Olson] + +* Documentation for AbstractRequest. #4895 [kevin.clark@gmail.com] + +* Remove all remaining references to @params in the documentation. [Marcel Molina Jr.] + +* Add documentation for redirect_to :back's RedirectBackError exception. [Marcel Molina Jr.] + +* Update layout and content_for documentation to use yield rather than magic @content_for instance variables. [Marcel Molina Jr.] + +* Cache CgiRequest#request_parameters so that multiple calls don't re-parse multipart data. [Rick] + +* Fixed that remote_form_for can leave out the object parameter and default to the instance variable of the object_name, just like form_for [DHH] + +* Added ActionController.filter_parameter_logging that makes it easy to remove passwords, credit card numbers, and other sensitive information from being logged when a request is handled. #1897 [jeremye@bsa.ca.gov] + +* Fixed that real files and symlinks should be treated the same when compiling templates. #5438 [zachary@panandscan.com] + +* Add :status option to send_data and send_file. Defaults to '200 OK'. #5243 [Manfred Stienstra ] + +* Update documentation for erb trim syntax. #5651 [matt@mattmargolis.net] + +* Short documentation to mention use of Mime::Type.register. #5710 [choonkeat@gmail.com] + + +*1.12.3* (June 28th, 2006) + +* Fix broken traverse_to_controller. We now: + Look for a _controller.rb file under RAILS_ROOT to load. + If we find it, we require_dependency it and return the controller it defined. (If none was defined we stop looking.) + If we don't find it, we look for a .rb file under RAILS_ROOT to load. If we find it, and it loads a constant we keep looking. + Otherwise we check to see if a directory of the same name exists, and if it does we create a module for it. + + +*1.12.2* (June 27th, 2006) + +* Refinement to avoid exceptions in traverse_to_controller. + +* (Hackish) Fix loading of arbitrary files in Ruby's load path by traverse_to_controller. [Nicholas Seckar] + + +*1.12.1* (April 6th, 2006) + +* Fixed that template extensions would be cached development mode #4624 [Stefan Kaes] + +* Update to Prototype 1.5.0_rc0 [Sam Stephenson] + +* Honor skipping filters conditionally for only certain actions even when the parent class sets that filter to conditionally be executed only for the same actions. #4522 [Marcel Molina Jr.] + +* Delegate xml_http_request in integration tests to the session instance. [Jamis Buck] + +* Update the diagnostics template skip the useless '' text. [Nicholas Seckar] + +* CHANGED DEFAULT: Don't parse YAML input by default, but keep it available as an easy option [DHH] + +* Add additional autocompleter options [aballai, Thomas Fuchs] + +* Fixed fragment caching of binary data on Windows #4493 [bellis@deepthought.org] + +* Applied Prototype $() performance patches (#4465, #4477) and updated script.aculo.us [Sam Stephenson, Thomas Fuchs] + +* Added automated timestamping to AssetTagHelper methods for stylesheets, javascripts, and images when Action Controller is run under Rails [DHH]. Example: + + image_tag("rails.png") # => 'Rails' + + ...to avoid frequent stats (not a problem for most people), you can set RAILS_ASSET_ID in the ENV to avoid stats: + + ENV["RAILS_ASSET_ID"] = "2345" + image_tag("rails.png") # => 'Rails' + + This can be used by deployment managers to set the asset id by application revision + + +*1.12.0* (March 27th, 2006) + +* Add documentation for respond_to. [Jamis Buck] + +* Fixed require of bluecloth and redcloth when gems haven't been loaded #4446 [murphy@cYcnus.de] + +* Update to Prototype 1.5.0_pre1 [Sam Stephenson] + +* Change #form_for and #fields_for so that the second argument is not required [Dave Thomas] + + <% form_for :post, @post, :url => { :action => 'create' } do |f| -%> + + becomes... + + <% form_for :post, :url => { :action => 'create' } do |f| -%> + +* Update to script.aculo.us 1.6 [Thomas Fuchs] + +* Enable application/x-yaml processing by default [Jamis Buck] + +* Fix double url escaping of remote_function. Add :escape => false option to ActionView's url_for. [Nicholas Seckar] + +* Add :script option to in_place_editor to support evalScripts (closes #4194) [codyfauser@gmail.com] + +* Fix mixed case enumerable methods in the JavaScript Collection Proxy (closes #4314) [codyfauser@gmail.com] + +* Undo accidental escaping for mail_to; add regression test. [Nicholas Seckar] + +* Added nicer message for assert_redirected_to (closes #4294) [court3nay] + + assert_redirected_to :action => 'other_host', :only_path => false + + when it was expecting... + + redirected_to :action => 'other_host', :only_path => true, :host => 'other.test.host' + + gives the error message... + + response is not a redirection to all of the options supplied (redirection is <{:only_path=>false, :host=>"other.test.host", :action=>"other_host"}>), difference: <{:only_path=>"true", :host=>"other.test.host"}> + +* Change url_for to escape the resulting URLs when called from a view. [Nicholas Seckar, coffee2code] + +* Added easy support for testing file uploads with fixture_file_upload #4105 [turnip@turnipspatch.com]. Example: + + # Looks in Test::Unit::TestCase.fixture_path + '/files/spongebob.png' + post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png') + +* Fixed UrlHelper#current_page? to behave even when url-escaped entities are present #3929 [jeremy@planetargon.com] + +* Add ability for relative_url_root to be specified via an environment variable RAILS_RELATIVE_URL_ROOT. [isaac@reuben.com, Nicholas Seckar] + +* Fixed link_to "somewhere", :post => true to produce valid XHTML by using the parentnode instead of document.body for the instant form #3007 [Bob Silva] + +* Added :function option to PrototypeHelper#observe_field/observe_form that allows you to call a function instead of submitting an ajax call as the trigger #4268 [jonathan@daikini.com] + +* Make Mime::Type.parse consider q values (if any) [Jamis Buck] + +* XML-formatted requests are typecast according to "type" attributes for :xml_simple [Jamis Buck] + +* Added protection against proxy setups treating requests as local even when they're not #3898 [stephen_purcell@yahoo.com] + +* Added TestRequest#raw_post that simulate raw_post from CgiRequest #3042 [francois.beausoleil@gmail.com] + +* Underscore dasherized keys in formatted requests [Jamis Buck] + +* Add MimeResponds::Responder#any for managing multiple types with identical responses [Jamis Buck] + +* Make the xml_http_request testing method set the HTTP_ACCEPT header [Jamis Buck] + +* Add Verification to scaffolds. Prevent destructive actions using GET [Michael Koziarski] + +* Avoid hitting the filesystem when using layouts by using a File.directory? cache. [Stefan Kaes, Nicholas Seckar] + +* Simplify ActionController::Base#controller_path [Nicholas Seckar] + +* Added simple alert() notifications for RJS exceptions when config.action_view.debug_rjs = true. [Sam Stephenson] + +* Added :content_type option to render, so you can change the content type on the fly [DHH]. Example: render :action => "atom.rxml", :content_type => "application/atom+xml" + +* CHANGED DEFAULT: The default content type for .rxml is now application/xml instead of type/xml, see http://www.xml.com/pub/a/2004/07/21/dive.html for reason [DHH] + +* Added option to render action/template/file of a specific extension (and here by template type). This means you can have multiple templates with the same name but a different extension [DHH]. Example: + + class WeblogController < ActionController::Base + def index + @posts = Post.find :all + + respond_to do |type| + type.html # using defaults, which will render weblog/index.rhtml + type.xml { render :action => "index.rxml" } + type.js { render :action => "index.rjs" } + end + end + end + +* Added better support for using the same actions to output for different sources depending on the Accept header [DHH]. Example: + + class WeblogController < ActionController::Base + def create + @post = Post.create(params[:post]) + + respond_to do |type| + type.js { render } # renders create.rjs + type.html { redirect_to :action => "index" } + type.xml do + headers["Location"] = url_for(:action => "show", :id => @post) + render(:nothing, :status => "201 Created") + end + end + end + end + +* Added Base#render(:xml => xml) that works just like Base#render(:text => text), but sets the content-type to text/xml and the charset to UTF-8 [DHH] + +* Integration test's url_for now runs in the context of the last request (if any) so after post /products/show/1 url_for :action => 'new' will yield /product/new [Tobias Luetke] + +* Re-added mixed-in helper methods for the JavascriptGenerator. Moved JavascriptGenerators methods to a module that is mixed in after the helpers are added. Also fixed that variables set in the enumeration methods like #collect are set correctly. Documentation added for the enumeration methods [Rick Olson]. Examples: + + page.select('#items li').collect('items') do |element| + element.hide + end + # => var items = $$('#items li').collect(function(value, index) { return value.hide(); }); + +* Added plugin support for parameter parsers, which allows for better support for REST web services. By default, posts submitted with the application/xml content type is handled by creating a XmlSimple hash with the same name as the root element of the submitted xml. More handlers can easily be registered like this: + + # Assign a new param parser to a new content type + ActionController::Base.param_parsers['application/atom+xml'] = Proc.new do |data| + node = REXML::Document.new(post) + { node.root.name => node.root } + end + + # Assign the default XmlSimple to a new content type + ActionController::Base.param_parsers['application/backpack+xml'] = :xml_simple + +Default YAML web services were retired, ActionController::Base.param_parsers carries an example which shows how to get this functionality back. As part of this new plugin support, request.[formatted_post?, xml_post?, yaml_post? and post_format] were all deprecated in favor of request.content_type [Tobias Luetke] + +* Fixed Effect.Appear in effects.js to work with floats in Safari #3524, #3813, #3044 [Thomas Fuchs] + +* Fixed that default image extension was not appended when using a full URL with AssetTagHelper#image_tag #4032, #3728 [rubyonrails@beautifulpixel.com] + +* Added that page caching will only happen if the response code is less than 400 #4033 [g.bucher@teti.ch] + +* Add ActionController::IntegrationTest to allow high-level testing of the way the controllers and routes all work together [Jamis Buck] + +* Added support to AssetTagHelper#javascript_include_tag for having :defaults appear anywhere in the list, so you can now make one call ala javascript_include_tag(:defaults, "my_scripts") or javascript_include_tag("my_scripts", :defaults) depending on how you want the load order #3506 [Bob Silva] + +* Added support for visual effects scoped queues to the visual_effect helper #3530 [Abdur-Rahman Advany] + +* Added .rxml (and any non-rhtml template, really) supportfor CaptureHelper#content_for and CaptureHelper#capture #3287 [Brian Takita] + +* Added script.aculo.us drag and drop helpers to RJS [Thomas Fuchs]. Examples: + + page.draggable 'product-1' + page.drop_receiving 'wastebasket', :url => { :action => 'delete' } + page.sortable 'todolist', :url => { action => 'change_order' } + +* Fixed that form elements would strip the trailing [] from the first parameter #3545 [ruby@bobsilva.com] + +* During controller resolution, update the NameError suppression to check for the expected constant. [Nicholas Seckar] + +* Update script.aculo.us to V1.5.3 [Thomas Fuchs] + +* Added various InPlaceEditor options, #3746, #3891, #3896, #3906 [Bill Burcham, ruairi, sl33p3r] + +* Added :count option to pagination that'll make it possible for the ActiveRecord::Base.count call to using something else than * for the count. Especially important for count queries using DISTINCT #3839 [skaes] + +* Update script.aculo.us to V1.5.2 [Thomas Fuchs] + +* Added element and collection proxies to RJS [DHH]. Examples: + + page['blank_slate'] # => $('blank_slate'); + page['blank_slate'].show # => $('blank_slate').show(); + page['blank_slate'].show('first').up # => $('blank_slate').show('first').up(); + + page.select('p') # => $$('p'); + page.select('p.welcome b').first # => $$('p.welcome b').first(); + page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide(); + +* Add JavaScriptGenerator#replace for replacing an element's "outer HTML". #3246 [tom@craz8.com, Sam Stephenson] + +* Remove over-engineered form_for code for a leaner implementation. [Nicholas Seckar] + +* Document form_for's :html option. [Nicholas Seckar] + +* Major components cleanup and speedup. #3527 [Stefan Kaes] + +* Fix problems with pagination and :include. [Kevin Clark] + +* Add ActiveRecordTestCase for testing AR integration. [Kevin Clark] + +* Add Unit Tests for pagination [Kevin Clark] + +* Add :html option for specifying form tag options in form_for. [Sam Stephenson] + +* Replace dubious controller parent class in filter docs. #3655, #3722 [info@rhalff.com, eigentone@gmail.com] + +* Don't interpret the :value option on text_area as an html attribute. Set the text_area's value. #3752 [gabriel@gironda.org] + +* Fix remote_form_for creates a non-ajax form. [Rick Olson] + +* Don't let arbitrary classes match as controllers -- a potentially dangerous bug. [Nicholas Seckar] + +* Fix Routing tests. Fix routing where failing to match a controller would prevent the rest of routes from being attempted. [Nicholas Seckar] + +* Add :builder => option to form_for and friends. [Nicholas Seckar, Rick Olson] + +* Fix controller resolution to avoid accidentally inheriting a controller from a parent module. [Nicholas Seckar] + +* Set sweeper's @controller to nil after a request so that the controller may be collected between requests. [Nicholas Seckar] + +* Subclasses of ActionController::Caching::Sweeper should be Reloadable. [Rick Olson] + +* Document the :xhr option for verifications. #3666 [leeo] + +* Added :only and :except controls to skip_before/after_filter just like for when you add filters [DHH] + +* Ensure that the instance variables are copied to the template when performing render :update. [Nicholas Seckar] + +* Add the ability to call JavaScriptGenerator methods from helpers called in update blocks. [Sam Stephenson] Example: + module ApplicationHelper + def update_time + page.replace_html 'time', Time.now.to_s(:db) + page.visual_effect :highlight, 'time' + end + end + + class UserController < ApplicationController + def poll + render :update { |page| page.update_time } + end + end + +* Add render(:update) to ActionView::Base. [Sam Stephenson] + +* Fix render(:update) to not render layouts. [Sam Stephenson] + +* Fixed that SSL would not correctly be detected when running lighttpd/fcgi behind lighttpd w/mod_proxy #3548 [stephen_purcell@yahoo.com] + +* Added the possibility to specify atomatic expiration for the memcachd session container #3571 [Stefan Kaes] + +* Change layout discovery to take into account the change in semantics with File.join and nil arguments. [Marcel Molina Jr.] + +* Raise a RedirectBackError if redirect_to :back is called when there's no HTTP_REFERER defined #3049 [kevin.clark@gmail.com] + +* Treat timestamps like datetimes for scaffolding purposes #3388 [Maik Schmidt] + +* Fix IE bug with link_to "something", :post => true #3443 [Justin Palmer] + +* Extract Test::Unit::TestCase test process behavior into an ActionController::TestProcess module. [Sam Stephenson] + +* Pass along blocks from render_to_string to render. [Sam Stephenson] + +* Add render :update for inline RJS. [Sam Stephenson] Example: + class UserController < ApplicationController + def refresh + render :update do |page| + page.replace_html 'user_list', :partial => 'user', :collection => @users + page.visual_effect :highlight, 'user_list' + end + end + end + +* allow nil objects for error_messages_for [Michael Koziarski] + +* Refactor human_size to exclude decimal place if it is zero. [Marcel Molina Jr.] + +* Update to Prototype 1.5.0_pre0 [Sam Stephenson] + +* Automatically discover layouts when a controller is namespaced. #2199, #3424 [me@jonnii.com rails@jeffcole.net Marcel Molina Jr.] + +* Add support for multiple proxy servers to CgiRequest#host [gaetanot@comcast.net] + +* Documentation typo fix. #2367 [Blair Zajac] + +* Remove Upload Progress. #2871 [Sean Treadway] + +* Fix typo in function name mapping in auto_complete_field. #2929 #3446 [doppler@gmail.com phil.ross@gmail.com] + +* Allow auto-discovery of third party template library layouts. [Marcel Molina Jr.] + +* Have the form builder output radio button, not check box, when calling the radio button helper. #3331 [LouisStAmour@gmail.com] + +* Added assignment of the Autocompleter object created by JavaScriptMacroHelper#auto_complete_field to a local javascript variables [DHH] + +* Added :on option for PrototypeHelper#observe_field that allows you to specify a different callback hook to have the observer trigger on [DHH] + +* Added JavaScriptHelper#button_to_function that works just like JavaScriptHelper#link_to_function but uses a button instead of a href [DHH] + +* Added that JavaScriptHelper#link_to_function will honor existing :onclick definitions when adding the function call [DHH] + +* Added :disable_with option to FormTagHelper#submit_tag to allow for easily disabled submit buttons with different text [DHH] + +* Make auto_link handle nil by returning quickly if blank? [Scott Barron] + +* Make auto_link match urls with a port number specified. [Marcel Molina Jr.] + +* Added support for toggling visual effects to ScriptaculousHelper::visual_effect, #3323. [Thomas Fuchs] + +* Update to script.aculo.us to 1.5.0 rev. 3343 [Thomas Fuchs] + +* Added :select option for JavaScriptMacroHelper#auto_complete_field that makes it easier to only use part of the auto-complete suggestion as the value for insertion [Thomas Fuchs] + +* Added delayed execution of Javascript from within RJS #3264 [devslashnull@gmail.com]. Example: + + page.delay(20) do + page.visual_effect :fade, 'notice' + end + +* Add session ID to default logging, but remove the verbose description of every step [DHH] + +* Add the following RJS methods: [Sam Stephenson] + + * alert - Displays an alert() dialog + * redirect_to - Changes window.location.href to simulate a browser redirect + * call - Calls a JavaScript function + * assign - Assigns to a JavaScript variable + * << - Inserts an arbitrary JavaScript string + +* Fix incorrect documentation for form_for [Nicholas Seckar] + +* Don't include a layout when rendering an rjs template using render's :template option. [Marcel Molina Jr.] + +*1.1.2* (December 13th, 2005) + +* Become part of Rails 1.0 + +* Update to script.aculo.us 1.5.0 final (equals 1.5.0_rc6) [Thomas Fuchs] + +* Update to Prototype 1.4.0 final [Sam Stephenson] + +* Added form_remote_for (form_for meets form_remote_tag) [DHH] + +* Update to script.aculo.us 1.5.0_rc6 + +* More robust relative url root discovery for SCGI compatibility. This solves the 'SCGI routes problem' -- you no longer need to prefix all your routes with the name of the SCGI mountpoint. #3070 [Dave Ringoen] + +* Fix docs for text_area_tag. #3083. [Christopher Cotton] + +* Change form_for and fields_for method signatures to take object name and object as separate arguments rather than as a Hash. [DHH] + +* Introduce :selected option to the select helper. Allows you to specify a selection other than the current value of object.method. Specify :selected => nil to leave all options unselected. #2991 [Jonathan Viney ] + +* Initialize @optional in routing code to avoid warnings about uninitialized access to an instance variable. [Nicholas Seckar] + +* Make ActionController's render honor the :locals option when rendering a :file. #1665. [Emanuel Borsboom, Marcel Molina Jr.] + +* Allow assert_tag(:conditions) to match the empty string when a tag has no children. Closes #2959. [Jamis Buck] + +* Update html-scanner to handle CDATA sections better. Closes #2970. [Jamis Buck] + +* Don't put flash in session if sessions are disabled. [Jeremy Kemper] + +* Strip out trailing &_= for raw post bodies. Closes #2868. [Sam Stephenson] + +* Pass multiple arguments to Element.show and Element.hide in JavaScriptGenerator instead of using iterators. [Sam Stephenson] + +* Improve expire_fragment documentation. #2966 [court3nay@gmail.com] + +* Correct docs for automatic layout assignment. #2610. [Charles M. Gerungan] + +* Always create new AR sessions rather than trying too hard to avoid database traffic. #2731 [Jeremy Kemper] + +* Update to Prototype 1.4.0_rc4. Closes #2943 (old Array.prototype.reverse behavior can be obtained by passing false as an argument). [Sam Stephenson] + +* Use Element.update('id', 'html') instead of $('id').innerHTML = 'html' in JavaScriptGenerator#replace_html so that script tags are evaluated. [Sam Stephenson] + +* Make rjs templates always implicitly skip out on layouts. [Marcel Molina Jr.] + +* Correct length for the truncate text helper. #2913 [Stefan Kaes] + +* Update to Prototype 1.4.0_rc3. Closes #1893, #2505, #2550, #2748, #2783. [Sam Stephenson] + +* Add support for new rjs templates which wrap an update_page block. [Marcel Molina Jr.] + +* Rename Version constant to VERSION. #2802 [Marcel Molina Jr.] + +* Correct time_zone_options_for_select docs. #2892 [pudeyo@rpi.com] + +* Remove the unused, slow response_dump and session_dump variables from error pages. #1222 [lmarlow@yahoo.com] + +* Performance tweaks: use Set instead of Array to speed up prototype helper include? calls. Avoid logging code if logger is nil. Inline commonly-called template presence checks. #2880, #2881, #2882, #2883 [Stefan Kaes] + +* MemCache store may be given multiple addresses. #2869 [Ryan Carver ] + +* Handle cookie parsing irregularity for certain Nokia phones. #2530 [zaitzow@gmail.com] + +* Added PrototypeHelper::JavaScriptGenerator and PrototypeHelper#update_page for easily modifying multiple elements in an Ajax response. [Sam Stephenson] Example: + + update_page do |page| + page.insert_html :bottom, 'list', '
  • Last item
  • ' + page.visual_effect :highlight, 'list' + page.hide 'status-indicator', 'cancel-link' + end + + generates the following JavaScript: + + new Insertion.Bottom("list", "
  • Last item
  • "); + new Effect.Highlight("list"); + ["status-indicator", "cancel-link"].each(Element.hide); + +* Refactored JavaScriptHelper into PrototypeHelper and ScriptaculousHelper [Sam Stephenson] + +* Update to latest script.aculo.us version (as of [3031]) + +* Updated docs for in_place_editor, fixes a couple bugs and offers extended support for external controls [Justin Palmer] + +* Update documentation for render :file. #2858 [Tom Werner] + +* Only include builtin filters whose filenames match /^[a-z][a-z_]*_helper.rb$/ to avoid including operating system metadata such as ._foo_helper.rb. #2855 [court3nay@gmail.com] + +* Added FormHelper#form_for and FormHelper#fields_for that makes it easier to work with forms for single objects also if they don't reside in instance variables [DHH]. Examples: + + <% form_for :person, @person, :url => { :action => "update" } do |f| %> + First name: <%= f.text_field :first_name %> + Last name : <%= f.text_field :last_name %> + Biography : <%= f.text_area :biography %> + Admin? : <%= f.check_box :admin %> + <% end %> + + <% form_for :person, person, :url => { :action => "update" } do |person_form| %> + First name: <%= person_form.text_field :first_name %> + Last name : <%= person_form.text_field :last_name %> + + <% fields_for :permission => person.permission do |permission_fields| %> + Admin? : <%= permission_fields.check_box :admin %> + <% end %> + <% end %> + +* options_for_select allows any objects which respond_to? :first and :last rather than restricting to Array and Range. #2824 [Jacob Robbins , Jeremy Kemper] + +* The auto_link text helper accepts an optional block to format the link text for each url and email address. Example: auto_link(post.body) { |text| truncate(text, 10) } [Jeremy Kemper] + +* assert_tag uses exact matches for string conditions, instead of partial matches. Use regex to do partial matches. #2799 [Jamis Buck] + +* CGI::Session::ActiveRecordStore.data_column_name = 'foobar' to use a different session data column than the 'data' default. [nbpwie102@sneakemail.com] + +* Do not raise an exception when default helper is missing; log a debug message instead. It's nice to delete empty helpers. [Jeremy Kemper] + +* Controllers with acronyms in their names (e.g. PDFController) require the correct default helper (PDFHelper in file pdf_helper.rb). #2262 [jeff@opendbms.com] + + +*1.11.0* (November 7th, 2005) + +* Added request as instance method to views, so you can do <%= request.env["HTTP_REFERER"] %>, just like you can already access response, session, and the likes [DHH] + +* Fix conflict with assert_tag and Glue gem #2255 [david.felstead@gmail.com] + +* Add documentation to assert_tag indicating that it only works with well-formed XHTML #1937, #2570 [Jamis Buck] + +* Added action_pack.rb stub so that ActionPack::Version loads properly [Sam Stephenson] + +* Added short-hand to assert_tag so assert_tag :tag => "span" can be written as assert_tag "span" [DHH] + +* Added skip_before_filter/skip_after_filter for easier control of the filter chain in inheritance hierachies [DHH]. Example: + + class ApplicationController < ActionController::Base + before_filter :authenticate + end + + class WeblogController < ApplicationController + # will run the :authenticate filter + end + + class SignupController < ActionController::Base + # will not run the :authenticate filter + skip_before_filter :authenticate + end + +* Added redirect_to :back as a short-hand for redirect_to(request.env["HTTP_REFERER"]) [DHH] + +* Change javascript_include_tag :defaults to not use script.aculo.us loader, which facilitates the use of plugins for future script.aculo.us and third party javascript extensions, and provide register_javascript_include_default for plugins to specify additional JavaScript files to load. Removed slider.js and builder.js from actionpack. [Thomas Fuchs] + +* Fix problem where redirecting components can cause an infinite loop [Rick Olson] + +* Added support for the queue option on visual_effect [Thomas Fuchs] + +* Update script.aculo.us to V1.5_rc4 [Thomas Fuchs] + +* Fix that render :text didn't interpolate instance variables #2629, #2626 [skaes] + +* Fix line number detection and escape RAILS_ROOT in backtrace Regexp [Nicholas Seckar] + +* Fixed document.getElementsByClassName from Prototype to be speedy again [Sam Stephenson] + +* Recognize ./#{RAILS_ROOT} as RAILS_ROOT in error traces [Nicholas Seckar] + +* Remove ARStore session fingerprinting [Nicholas Seckar] + +* Fix obscure bug in ARStore [Nicholas Seckar] + +* Added TextHelper#strip_tags for removing HTML tags from a string (using HTMLTokenizer) #2229 [marcin@junkheap.net] + +* Added a reader for flash.now, so it's possible to do stuff like flash.now[:alert] ||= 'New if not set' #2422 [Caio Chassot] + + +*1.10.2* (October 26th, 2005) + +* Reset template variables after using render_to_string [skaes@web.de] + +* Expose the session model backing CGI::Session + +* Abbreviate RAILS_ROOT in traces + + +*1.10.1* (October 19th, 2005) + +* Update error trace templates [Nicholas Seckar] + +* Stop showing generated routing code in application traces [Nicholas Seckar] + + +*1.10.0* (October 16th, 2005) + +* Make string-keys locals assigns optional. Add documentation describing depreciated state [skaes@web.de] + +* Improve line number detection for template errors [Nicholas Seckar] + +* Update/clean up documentation (rdoc) + +* Upgrade to Prototype 1.4.0_rc0 [Sam Stephenson] + +* Added assert_vaild. Reports the proper AR error messages as fail message when the passed record is invalid [Tobias Luetke] + +* Add temporary support for passing locals to render using string keys [Nicholas Seckar] + +* Clean up error pages by providing better backtraces [Nicholas Seckar] + +* Raise an exception if an attempt is made to insert more session data into the ActiveRecordStore data column than the column can hold. #2234. [justin@textdrive.com] + +* Removed references to assertions.rb from actionpack assert's backtraces. Makes error reports in functional unit tests much less noisy. [Tobias Luetke] + +* Updated and clarified documentation for JavaScriptHelper to be more concise about the various options for including the JavaScript libs. [Thomas Fuchs] + +* Hide "Retry with Breakpoint" button on error pages until feature is functional. [DHH] + +* Fix Request#host_with_port to use the standard port when Rails is behind a proxy. [Nicholas Seckar] + +* Escape query strings in the href attribute of URLs created by url_helper. #2333 [Michael Schuerig ] + +* Improved line number reporting for template errors [Nicholas Seckar] + +* Added :locals support for render :inline #2463 [mdabney@cavoksolutions.com] + +* Unset the X-Requested-With header when using the xhr wrapper in functional tests so that future requests aren't accidentally xhr'ed #2352 [me@julik.nl, Sam Stephenson] + +* Unescape paths before writing cache to file system. #1877. [Damien Pollet] + +* Wrap javascript_tag contents in a CDATA section and add a cdata_section method to TagHelper #1691 [Michael Schuerig, Sam Stephenson] + +* Misc doc fixes (typos/grammar/etc). #2445. [coffee2code] + +* Speed improvement for session_options. #2287. [skaes@web.de] + +* Make cacheing binary files friendly with Windows. #1975. [Rich Olson] + +* Convert boolean form options form the tag_helper. #809. [Michael Schuerig ] + +* Fixed that an instance variable with the same name as a partial should be implicitly passed as the partial :object #2269 [court3nay] + +* Update Prototype to V1.4.0_pre11, script.aculo.us to [2502] [Thomas Fuchs] + +* Make assert_tag :children count appropriately. Closes #2181. [jamie@bravenet.com] + +* Forced newer versions of RedCloth to use hard breaks [DHH] + +* Added new scriptaculous options for auto_complete_field #2343 [m.stienstra@fngtps.com] + +* Don't prepend the asset host if the string is already a fully-qualified URL + +* Updated to script.aculo.us V1.5.0_rc2 and Prototype to V1.4.0_pre7 [Thomas Fuchs] + +* Undo condition change made in [2345] to prevent normal parameters arriving as StringIO. + +* Tolerate consecutive delimiters in query parameters. #2295 [darashi@gmail.com] + +* Streamline render process, code cleaning. Closes #2294. [skae] + +* Keep flash after components are rendered. #2291 [Rick Olson, Scott] + +* Shorten IE file upload path to filename only to match other browsers. #1507 [court3nay@gmail.com] + +* Fix open/save dialog in IE not opening files send with send_file/send_data, #2279 [Thomas Fuchs] + +* Fixed that auto_discovery_link_tag couldn't take a string as the URL [DHH] + +* Fixed problem with send_file and WEBrick using stdout #1812 [DHH] + +* Optimized tag_options to not sort keys, which is no longer necessary when assert_dom_equal and friend is available #1995 [skae] + +* Added assert_dom_equal and assert_dom_not_equal to compare tags generated by the helpers in an order-indifferent manner #1995 [skae] + +* Fixed that Request#domain caused an exception if the domain header wasn't set in the original http request #1795 [Michael Koziarski] + +* Make the truncate() helper multi-byte safe (assuming $KCODE has been set to something other than "NONE") #2103 + +* Add routing tests from #1945 [ben@groovie.org] + +* Add a routing test case covering #2101 [Nicholas Seckar] + +* Cache relative_url_root for all webservers, not just Apache #2193 [skae] + +* Speed up cookie use by decreasing string copying #2194 [skae] + +* Fixed access to "Host" header with requests made by crappy old HTTP/1.0 clients #2124 [Marcel Molina] + +* Added easy assignment of fragment cache store through use of symbols for included stores (old way still works too) + + Before: + ActionController::Base.fragment_cache_store = + ActionController::Base::Caching::Fragments::FileStore.new("/path/to/cache/directory") + + After: + ActionController::Base.fragment_cache_store = :file_store, "/path/to/cache/directory" + +* Added ActionController::Base.session_store=, session_store, and session_options to make it easier to tweak the session options (instead of going straight to ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS) + +* Added TextHelper#cycle to cycle over an array of values on each hit (useful for alternating row colors etc) #2154 [dave-ml@dribin.org] + +* Ensure that request.path never returns nil. Closes #1675 [Nicholas Seckar] + +* Add ability to specify Route Regexps for controllers. Closes #1917. [Sebastian Kanthak] + +* Provide Named Route's hash methods as helper methods. Closes #1744. [Nicholas Seckar, Steve Purcell] + +* Added :multipart option to ActiveRecordHelper#form to make it possible to add file input fields #2034 [jstirk@oobleyboo.com] + +* Moved auto-completion and in-place editing into the Macros module and their helper counterparts into JavaScriptMacrosHelper + +* Added in-place editing support in the spirit of auto complete with ActionController::Base.in_place_edit_for, JavascriptHelper#in_place_editor_field, and Javascript support from script.aculo.us #2038 [Jon Tirsen] + +* Added :disabled option to all data selects that'll make the elements inaccessible for change #2167, #253 [eigentone] + +* Fixed that TextHelper#auto_link_urls would include punctuation in the links #2166, #1671 [eigentone] + +* Fixed that number_to_currency(1000, {:precision => 0})) should return "$1,000", instead of "$1,000." #2122 [sd@notso.net] + +* Allow link_to_remote to use any DOM-element as the parent of the form elements to be submitted #2137 [erik@ruby-lang.nl]. Example: + + + + + <%= link_to_remote 'Save', :update => "row023", + :submit => "row023", :url => {:action => 'save_row'} %> + + +* Fixed that render :partial would fail when :object was a Hash (due to backwards compatibility issues) #2148 [Sam Stephenson] + +* Fixed JavascriptHelper#auto_complete_for to only include unique items #2153 [Thomas Fuchs] + +* Fixed all AssetHelper methods to work with relative paths, such that javascript_include_tag('stdlib/standard') will look in /javascripts/stdlib/standard instead of '/stdlib/standard/' #1963 + +* Avoid extending view instance with helper modules each request. Closes #1979 + +* Performance improvements to CGI methods. Closes #1980 [Skaes] + +* Added :post option to UrlHelper#link_to that makes it possible to do POST requests through normal ahref links using Javascript + +* Fixed overwrite_params + +* Added ActionController::Base.benchmark and ActionController::Base.silence to allow for easy benchmarking and turning off the log + +* Updated vendor copy of html-scanner to support better xml parsing + +* Added :popup option to UrlHelper#link_to #1996 [gabriel.gironda@gmail.com]. Examples: + + link_to "Help", { :action => "help" }, :popup => true + link_to "Busy loop", { :action => "busy" }, :popup => ['new_window', 'height=300,width=600'] + +* Drop trailing \000 if present on RAW_POST_DATA (works around bug in Safari Ajax implementation) #918 + +* Fix observe_field to fall back to event-based observation if frequency <= 0 #1916 [michael@schubert.cx] + +* Allow use of the :with option for submit_to_remote #1936 [jon@instance-design.co.uk] + +* AbstractRequest#domain returns nil when host is an ip address #2012 [kevin.clark@gmail.com] + +* ActionController documentation update #2051 [fbeausoleil@ftml.net] + +* Yield @content_for_ variables to templates #2058 [Sam Stephenson] + +* Make rendering an empty partial collection behave like :nothing => true #2080 [Sam Stephenson] + +* Add option to specify the singular name used by pagination. + +* Use string key to obtain action value. Allows indifferent hashes to be disabled. + +* Added ActionView::Base.cache_template_loading back. + +* Rewrote compiled templates to decrease code complexity. Removed template load caching in favour of compiled caching. Fixed template error messages. [Nicholas Seckar] + +* Fix Routing to handle :some_param => nil better. [Nicholas Seckar, Luminas] + +* Add support for :include with pagination (subject to existing constraints for :include with :limit and :offset) #1478 [michael@schubert.cx] + +* Prevent the benchmark module from blowing up if a non-HTTP/1.1 request is processed + +* Added :use_short_month option to select_month helper to show month names as abbreviations + +* Make link_to escape the javascript in the confirm option #1964 [nicolas.pouillard@gmail.com] + +* Make assert_redirected_to properly check URL's passed as strings #1910 [Scott Barron] + +* Make sure :layout => false is always used when rendering inside a layout + +* Use raise instead of assert_not_nil in Test::Unit::TestCase#process to ensure that the test variables (controller, request, response) have been set + +* Make sure assigns are built for every request when testing #1866 + +* Allow remote_addr to be queried on TestRequest #1668 + +* Fixed bug when a partial render was passing a local with the same name as the partial + +* Improved performance of test app req/sec with ~10% refactoring the render method #1823 [Stefan Kaes] + +* Improved performance of test app req/sec with 5-30% through a series of Action Pack optimizations #1811 [Stefan Kaes] + +* Changed caching/expiration/hit to report using the DEBUG log level and errors to use the ERROR log level instead of both using INFO + +* Added support for per-action session management #1763 + +* Improved rendering speed on complicated templates by up to 100% (the more complex the templates, the higher the speedup) #1234 [Stephan Kaes]. This did necessasitate a change to the internals of ActionView#render_template that now has four parameters. Developers of custom view handlers (like Amrita) need to update for that. + +* Added options hash as third argument to FormHelper#input, so you can do input('person', 'zip', :size=>10) #1719 [jeremye@bsa.ca.gov] + +* Added Base#expires_in(seconds)/Base#expires_now to control HTTP content cache headers #1755 [Thomas Fuchs] + +* Fixed line number reporting for Builder template errors #1753 [piotr] + +* Fixed assert_routing so that testing controllers in modules works as expected [Nicholas Seckar, Rick Olson] + +* Fixed bug with :success/:failure callbacks for the JavaScriptHelper methods #1730 [court3nay/Thomas Fuchs] + +* Added named_route method to RouteSet instances so that RouteSet instance methods do not prevent certain names from being used. [Nicholas Seckar] + +* Fixed routes so that routes which do not specify :action in the path or in the requirements have a default of :action => 'index', In addition, fixed url generation so that :action => 'index' does not need to be provided for such urls. [Nicholas Seckar, Markjuh] + +* Worked around a Safari bug where it wouldn't pass headers through if the response was zero length by having render :nothing return ' ' instead of '' + +* Fixed Request#subdomains to handle "foo.foo.com" correctly + + +*1.9.1* (11 July, 2005) + +* Fixed that auto_complete_for didn't force the input string to lower case even as the db comparison was + +* Fixed that Action View should always use the included Builder, never attempt to require the gem, to ensure compatibility + +* Added that nil options are not included in tags, so tag("p", :ignore => nil) now returns

    not

    but that tag("p", :ignore => "") still includes it #1465 [michael@schuerig.de] + +* Fixed that UrlHelper#link_to_unless/link_to_if used html_escape on the name if no link was to be applied. This is unnecessary and breaks its use with images #1649 [joergd@pobox.com] + +* Improved error message for DoubleRenderError + +* Fixed routing to allow for testing of *path components #1650 [Nicholas Seckar] + +* Added :handle as an option to sortable_element to restrict the drag handle to a given class #1642 [thejohnny] + +* Added a bunch of script.aculo.us features #1644, #1677, #1695 [Thomas Fuchs] + * Effect.ScrollTo, to smoothly scroll the page to an element + * Better Firefox flickering handling on SlideUp/SlideDown + * Removed a possible memory leak in IE with draggables + * Added support for cancelling dragging my hitting ESC + * Added capability to remove draggables/droppables and redeclare sortables in dragdrop.js (this makes it possible to call sortable_element on the same element more than once, e.g. in AJAX returns that modify the sortable element. all current sortable 'stuff' on the element will be discarded and the sortable will be rebuilt) + * Always reset background color on Effect.Highlight; this make change backwards-compatibility, to be sure include style="background-color:(target-color)" on your elements or else elements will fall back to their CSS rules (which is a good thing in most circumstances) + * Removed circular references from element to prevent memory leaks (still not completely gone in IE) + * Changes to class extension in effects.js + * Make Effect.Highlight restore any previously set background color when finishing (makes effect work with CSS classes that set a background color) + * Fixed myriads of memory leaks in IE and Gecko-based browsers [David Zülke] + * Added incremental and local autocompleting and loads of documentation to controls.js [Ivan Krstic] + * Extended the auto_complete_field helper to accept tokens option + * Changed object extension mechanism to favor Object.extend to make script.aculo.us easily adaptable to support 3rd party libs like IE7.js [David Zülke] + +* Fixed that named routes didn't use the default values for action and possible other parameters #1534 [Nicholas Seckar] + +* Fixed JavascriptHelper#visual_effect to use camelize such that :blind_up will work #1639 [pelletierm@eastmedia.net] + +* Fixed that a SessionRestoreError was thrown if a model object was placed in the session that wasn't available to all controllers. This means that it's no longer necessary to use the 'model :post' work-around in ApplicationController to have a Post model in your session. + + +*1.9.0* (6 July, 2005) + +* Added logging of the request URI in the benchmark statement (makes it easy to grep for slow actions) + +* Added javascript_include_tag :defaults shortcut that'll include all the default javascripts included with Action Pack (prototype, effects, controls, dragdrop) + +* Cache several controller variables that are expensive to calculate #1229 [skaes@web.de] + +* The session class backing CGI::Session::ActiveRecordStore may be replaced with any class that duck-types with a subset of Active Record. See docs for details #1238 [skaes@web.de] + +* Fixed that hashes was not working properly when passed by GET to lighttpd #849 [Nicholas Seckar] + +* Fixed assert_template nil will be true when no template was rendered #1565 [maceywj@telus.net] + +* Added :prompt option to FormOptions#select (and the users of it, like FormOptions#select_country etc) to create "Please select" style descriptors #1181 [Michael Schuerig] + +* Added JavascriptHelper#update_element_function, which returns a Javascript function (or expression) that'll update a DOM element according to the options passed #933 [mortonda@dgrmm.net]. Examples: + + <%= update_element_function("products", :action => :insert, :position => :bottom, :content => "

    New product!

    ") %> + + <% update_element_function("products", :action => :replace, :binding => binding) do %> +

    Product 1

    +

    Product 2

    + <% end %> + +* Added :field_name option to DateHelper#select_(year|month|day) to deviate from the year/month/day defaults #1266 [Marcel Molina] + +* Added JavascriptHelper#draggable_element and JavascriptHelper#drop_receiving_element to facilitate easy dragging and dropping through the script.aculo.us libraries #1578 [Thomas Fuchs] + +* Added that UrlHelper#mail_to will now also encode the default link title #749 [f.svehla@gmail.com] + +* Removed the default option of wrap=virtual on FormHelper#text_area to ensure XHTML compatibility #1300 [thomas@columbus.rr.com] + +* Adds the ability to include XML CDATA tags using Builder #1563 [Josh Knowles]. Example: + + xml.cdata! "some text" # => + +* Added evaluation of + # + # javascript_include_tag "common.javascript", "/elsewhere/cools" # => + # + # + # + # javascript_include_tag :defaults # => + # + # + # ... + # *see below + # + # If there's an application.js file in your public/javascripts directory, + # javascript_include_tag :defaults will automatically include it. This file + # facilitates the inclusion of small snippets of JavaScript code, along the lines of + # controllers/application.rb and helpers/application_helper.rb. + def javascript_include_tag(*sources) + options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { } + + if sources.include?(:defaults) + sources = sources[0..(sources.index(:defaults))] + + @@javascript_default_sources.dup + + sources[(sources.index(:defaults) + 1)..sources.length] + + sources.delete(:defaults) + sources << "application" if defined?(RAILS_ROOT) && File.exists?("#{RAILS_ROOT}/public/javascripts/application.js") + end + + sources.collect { |source| + source = javascript_path(source) + content_tag("script", "", { "type" => "text/javascript", "src" => source }.merge(options)) + }.join("\n") + end + + # Register one or more additional JavaScript files to be included when + # + # javascript_include_tag :defaults + # + # is called. This method is intended to be called only from plugin initialization + # to register extra .js files the plugin installed in public/javascripts. + def self.register_javascript_include_default(*sources) + @@javascript_default_sources.concat(sources) + end + + def self.reset_javascript_include_default #:nodoc: + @@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup + end + + # Returns path to a stylesheet asset. Example: + # + # stylesheet_path "style" # => /stylesheets/style.css + def stylesheet_path(source) + compute_public_path(source, 'stylesheets', 'css') + end + + # Returns a css link tag per source given as argument. Examples: + # + # stylesheet_link_tag "style" # => + # + # + # stylesheet_link_tag "style", :media => "all" # => + # + # + # stylesheet_link_tag "random.styles", "/css/stylish" # => + # + # + def stylesheet_link_tag(*sources) + options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { } + sources.collect { |source| + source = stylesheet_path(source) + tag("link", { "rel" => "Stylesheet", "type" => "text/css", "media" => "screen", "href" => source }.merge(options)) + }.join("\n") + end + + # Returns path to an image asset. Example: + # + # The +src+ can be supplied as a... + # * full path, like "/my_images/image.gif" + # * file name, like "rss.gif", that gets expanded to "/images/rss.gif" + # * file name without extension, like "logo", that gets expanded to "/images/logo.png" + def image_path(source) + compute_public_path(source, 'images', 'png') + end + + # Returns an image tag converting the +options+ into html options on the tag, but with these special cases: + # + # * :alt - If no alt text is given, the file name part of the +src+ is used (capitalized and without the extension) + # * :size - Supplied as "XxY", so "30x45" becomes width="30" and height="45" + # + # The +src+ can be supplied as a... + # * full path, like "/my_images/image.gif" + # * file name, like "rss.gif", that gets expanded to "/images/rss.gif" + # * file name without extension, like "logo", that gets expanded to "/images/logo.png" + def image_tag(source, options = {}) + options.symbolize_keys! + + options[:src] = image_path(source) + options[:alt] ||= File.basename(options[:src], '.*').split('.').first.capitalize + + if options[:size] + options[:width], options[:height] = options[:size].split("x") + options.delete :size + end + + tag("img", options) + end + + private + def compute_public_path(source, dir, ext) + source = "/#{dir}/#{source}" unless source.first == "/" || source.include?(":") + source << ".#{ext}" unless source.split("/").last.include?(".") + source << '?' + rails_asset_id(source) if defined?(RAILS_ROOT) && %r{^[-a-z]+://} !~ source + source = "#{@controller.request.relative_url_root}#{source}" unless %r{^[-a-z]+://} =~ source + source = ActionController::Base.asset_host + source unless source.include?(":") + source + end + + def rails_asset_id(source) + ENV["RAILS_ASSET_ID"] || + File.mtime("#{RAILS_ROOT}/public/#{source}").to_i.to_s rescue "" + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/benchmark_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/benchmark_helper.rb new file mode 100644 index 00000000..1d53be51 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/benchmark_helper.rb @@ -0,0 +1,24 @@ +require 'benchmark' + +module ActionView + module Helpers + module BenchmarkHelper + # Measures the execution time of a block in a template and reports the result to the log. Example: + # + # <% benchmark "Notes section" do %> + # <%= expensive_notes_operation %> + # <% end %> + # + # Will add something like "Notes section (0.34523)" to the log. + # + # You may give an optional logger level as the second argument + # (:debug, :info, :warn, :error). The default is :info. + def benchmark(message = "Benchmarking", level = :info) + if @logger + real = Benchmark.realtime { yield } + @logger.send level, "#{message} (#{'%.5f' % real})" + end + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/cache_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/cache_helper.rb new file mode 100644 index 00000000..de2707ac --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/cache_helper.rb @@ -0,0 +1,10 @@ +module ActionView + module Helpers + # See ActionController::Caching::Fragments for usage instructions. + module CacheHelper + def cache(name = {}, &block) + @controller.cache_erb_fragment(block, name) + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/capture_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/capture_helper.rb new file mode 100644 index 00000000..28b3e299 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/capture_helper.rb @@ -0,0 +1,128 @@ +module ActionView + module Helpers + # Capture lets you extract parts of code which + # can be used in other points of the template or even layout file. + # + # == Capturing a block into an instance variable + # + # <% @script = capture do %> + # [some html...] + # <% end %> + # + # == Add javascript to header using content_for + # + # content_for("name") is a wrapper for capture which will + # make the fragment available by name to a yielding layout or template. + # + # layout.rhtml: + # + # + # + # layout with js + # + # + # + # <%= yield %> + # + # + # + # view.rhtml + # + # This page shows an alert box! + # + # <% content_for("script") do %> + # alert('hello world') + # <% end %> + # + # Normal view text + module CaptureHelper + # Capture allows you to extract a part of the template into an + # instance variable. You can use this instance variable anywhere + # in your templates and even in your layout. + # + # Example of capture being used in a .rhtml page: + # + # <% @greeting = capture do %> + # Welcome To my shiny new web page! + # <% end %> + # + # Example of capture being used in a .rxml page: + # + # @greeting = capture do + # 'Welcome To my shiny new web page!' + # end + def capture(*args, &block) + # execute the block + begin + buffer = eval("_erbout", block.binding) + rescue + buffer = nil + end + + if buffer.nil? + capture_block(*args, &block) + else + capture_erb_with_buffer(buffer, *args, &block) + end + end + + # Calling content_for stores the block of markup for later use. + # Subsequently, you can make calls to it by name with yield + # in another template or in the layout. + # + # Example: + # + # <% content_for("header") do %> + # alert('hello world') + # <% end %> + # + # You can use yield :header anywhere in your templates. + # + # <%= yield :header %> + # + # NOTE: Beware that content_for is ignored in caches. So you shouldn't use it + # for elements that are going to be fragment cached. + # + # The deprecated way of accessing a content_for block was to use a instance variable + # named @@content_for_#{name_of_the_content_block}@. So <%= content_for('footer') %> + # would be avaiable as <%= @content_for_footer %>. The preferred notation now is + # <%= yield :footer %>. + def content_for(name, &block) + eval "@content_for_#{name} = (@content_for_#{name} || '') + capture(&block)" + end + + private + def capture_block(*args, &block) + block.call(*args) + end + + def capture_erb(*args, &block) + buffer = eval("_erbout", block.binding) + capture_erb_with_buffer(buffer, *args, &block) + end + + def capture_erb_with_buffer(buffer, *args, &block) + pos = buffer.length + block.call(*args) + + # extract the block + data = buffer[pos..-1] + + # replace it in the original with empty string + buffer[pos..-1] = '' + + data + end + + def erb_content_for(name, &block) + eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_erb(&block)" + end + + def block_content_for(name, &block) + eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_block(&block)" + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/date_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/date_helper.rb new file mode 100755 index 00000000..00400956 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/date_helper.rb @@ -0,0 +1,307 @@ +require "date" + +module ActionView + module Helpers + # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the select-type methods + # share a number of common options that are as follows: + # + # * :prefix - overwrites the default prefix of "date" used for the select names. So specifying "birthday" would give + # birthday[month] instead of date[month] if passed to the select_month method. + # * :include_blank - set to true if it should be possible to set an empty date. + # * :discard_type - set to true if you want to discard the type part of the select name. If set to true, the select_month + # method would use simply "date" (which can be overwritten using :prefix) instead of "date[month]". + module DateHelper + DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX') + + # Reports the approximate distance in time between two Time objects or integers. + # For example, if the distance is 47 minutes, it'll return + # "about 1 hour". See the source for the complete wording list. + # + # Integers are interpreted as seconds. So, + # distance_of_time_in_words(50) returns "less than a minute". + # + # Set include_seconds to true if you want more detailed approximations if distance < 1 minute + def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false) + from_time = from_time.to_time if from_time.respond_to?(:to_time) + to_time = to_time.to_time if to_time.respond_to?(:to_time) + distance_in_minutes = (((to_time - from_time).abs)/60).round + distance_in_seconds = ((to_time - from_time).abs).round + + case distance_in_minutes + when 0..1 + return (distance_in_minutes==0) ? 'less than a minute' : '1 minute' unless include_seconds + case distance_in_seconds + when 0..5 then 'less than 5 seconds' + when 6..10 then 'less than 10 seconds' + when 11..20 then 'less than 20 seconds' + when 21..40 then 'half a minute' + when 41..59 then 'less than a minute' + else '1 minute' + end + + when 2..45 then "#{distance_in_minutes} minutes" + when 46..90 then 'about 1 hour' + when 90..1440 then "about #{(distance_in_minutes.to_f / 60.0).round} hours" + when 1441..2880 then '1 day' + else "#{(distance_in_minutes / 1440).round} days" + end + end + + # Like distance_of_time_in_words, but where to_time is fixed to Time.now. + def time_ago_in_words(from_time, include_seconds = false) + distance_of_time_in_words(from_time, Time.now, include_seconds) + end + + alias_method :distance_of_time_in_words_to_now, :time_ago_in_words + + # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute (identified by + # +method+) on an object assigned to the template (identified by +object+). It's possible to tailor the selects through the +options+ hash, + # which accepts all the keys that each of the individual select builders do (like :use_month_numbers for select_month) as well as a range of + # discard options. The discard options are :discard_year, :discard_month and :discard_day. Set to true, they'll + # drop the respective select. Discarding the month select will also automatically discard the day select. It's also possible to explicitly + # set the order of the tags using the :order option with an array of symbols :year, :month and :day in + # the desired order. Symbols may be omitted and the respective select is not included. + # + # Passing :disabled => true as part of the +options+ will make elements inaccessible for change. + # + # NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed. + # + # Examples: + # + # date_select("post", "written_on") + # date_select("post", "written_on", :start_year => 1995) + # date_select("post", "written_on", :start_year => 1995, :use_month_numbers => true, + # :discard_day => true, :include_blank => true) + # date_select("post", "written_on", :order => [:day, :month, :year]) + # date_select("user", "birthday", :order => [:month, :day]) + # + # The selects are prepared for multi-parameter assignment to an Active Record object. + def date_select(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_date_select_tag(options) + end + + # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based + # attribute (identified by +method+) on an object assigned to the template (identified by +object+). Examples: + # + # datetime_select("post", "written_on") + # datetime_select("post", "written_on", :start_year => 1995) + # + # The selects are prepared for multi-parameter assignment to an Active Record object. + def datetime_select(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_datetime_select_tag(options) + end + + # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+. + def select_date(date = Date.today, options = {}) + select_year(date, options) + select_month(date, options) + select_day(date, options) + end + + # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the +datetime+. + def select_datetime(datetime = Time.now, options = {}) + select_year(datetime, options) + select_month(datetime, options) + select_day(datetime, options) + + select_hour(datetime, options) + select_minute(datetime, options) + end + + # Returns a set of html select-tags (one for hour and minute) + def select_time(datetime = Time.now, options = {}) + h = select_hour(datetime, options) + select_minute(datetime, options) + (options[:include_seconds] ? select_second(datetime, options) : '') + end + + # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected. + # The second can also be substituted for a second number. + # Override the field name using the :field_name option, 'second' by default. + def select_second(datetime, options = {}) + second_options = [] + + 0.upto(59) do |second| + second_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) == second) ? + %(\n) : + %(\n) + ) + end + + select_html(options[:field_name] || 'second', second_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) + end + + # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected. + # Also can return a select tag with options by minute_step from 0 through 59 with the 00 minute selected + # The minute can also be substituted for a minute number. + # Override the field name using the :field_name option, 'minute' by default. + def select_minute(datetime, options = {}) + minute_options = [] + + 0.step(59, options[:minute_step] || 1) do |minute| + minute_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.min) == minute) ? + %(\n) : + %(\n) + ) + end + + select_html(options[:field_name] || 'minute', minute_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) + end + + # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. + # The hour can also be substituted for a hour number. + # Override the field name using the :field_name option, 'hour' by default. + def select_hour(datetime, options = {}) + hour_options = [] + + 0.upto(23) do |hour| + hour_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) == hour) ? + %(\n) : + %(\n) + ) + end + + select_html(options[:field_name] || 'hour', hour_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) + end + + # Returns a select tag with options for each of the days 1 through 31 with the current day selected. + # The date can also be substituted for a hour number. + # Override the field name using the :field_name option, 'day' by default. + def select_day(date, options = {}) + day_options = [] + + 1.upto(31) do |day| + day_options << ((date && (date.kind_of?(Fixnum) ? date : date.day) == day) ? + %(\n) : + %(\n) + ) + end + + select_html(options[:field_name] || 'day', day_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) + end + + # Returns a select tag with options for each of the months January through December with the current month selected. + # The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are used as values + # (what's submitted to the server). It's also possible to use month numbers for the presentation instead of names -- + # set the :use_month_numbers key in +options+ to true for this to happen. If you want both numbers and names, + # set the :add_month_numbers key in +options+ to true. Examples: + # + # select_month(Date.today) # Will use keys like "January", "March" + # select_month(Date.today, :use_month_numbers => true) # Will use keys like "1", "3" + # select_month(Date.today, :add_month_numbers => true) # Will use keys like "1 - January", "3 - March" + # + # Override the field name using the :field_name option, 'month' by default. + # + # If you would prefer to show month names as abbreviations, set the + # :use_short_month key in +options+ to true. + def select_month(date, options = {}) + month_options = [] + month_names = options[:use_short_month] ? Date::ABBR_MONTHNAMES : Date::MONTHNAMES + + 1.upto(12) do |month_number| + month_name = if options[:use_month_numbers] + month_number + elsif options[:add_month_numbers] + month_number.to_s + ' - ' + month_names[month_number] + else + month_names[month_number] + end + + month_options << ((date && (date.kind_of?(Fixnum) ? date : date.month) == month_number) ? + %(\n) : + %(\n) + ) + end + + select_html(options[:field_name] || 'month', month_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) + end + + # Returns a select tag with options for each of the five years on each side of the current, which is selected. The five year radius + # can be changed using the :start_year and :end_year keys in the +options+. Both ascending and descending year + # lists are supported by making :start_year less than or greater than :end_year. The date can also be + # substituted for a year given as a number. Example: + # + # select_year(Date.today, :start_year => 1992, :end_year => 2007) # ascending year values + # select_year(Date.today, :start_year => 2005, :end_year => 1900) # descending year values + # + # Override the field name using the :field_name option, 'year' by default. + def select_year(date, options = {}) + year_options = [] + y = date ? (date.kind_of?(Fixnum) ? (y = (date == 0) ? Date.today.year : date) : date.year) : Date.today.year + + start_year, end_year = (options[:start_year] || y-5), (options[:end_year] || y+5) + step_val = start_year < end_year ? 1 : -1 + + start_year.step(end_year, step_val) do |year| + year_options << ((date && (date.kind_of?(Fixnum) ? date : date.year) == year) ? + %(\n) : + %(\n) + ) + end + + select_html(options[:field_name] || 'year', year_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) + end + + private + def select_html(type, options, prefix = nil, include_blank = false, discard_type = false, disabled = false) + select_html = %(\n" + end + + def leading_zero_on_single_digits(number) + number > 9 ? number : "0#{number}" + end + end + + class InstanceTag #:nodoc: + include DateHelper + + def to_date_select_tag(options = {}) + defaults = { :discard_type => true } + options = defaults.merge(options) + options_with_prefix = Proc.new { |position| options.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") } + date = options[:include_blank] ? (value || 0) : (value || Date.today) + + date_select = '' + options[:order] = [:month, :year, :day] if options[:month_before_year] # For backwards compatibility + options[:order] ||= [:year, :month, :day] + + position = {:year => 1, :month => 2, :day => 3} + + discard = {} + discard[:year] = true if options[:discard_year] + discard[:month] = true if options[:discard_month] + discard[:day] = true if options[:discard_day] or options[:discard_month] + + options[:order].each do |param| + date_select << self.send("select_#{param}", date, options_with_prefix.call(position[param])) unless discard[param] + end + + date_select + end + + def to_datetime_select_tag(options = {}) + defaults = { :discard_type => true } + options = defaults.merge(options) + options_with_prefix = Proc.new { |position| options.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") } + datetime = options[:include_blank] ? (value || nil) : (value || Time.now) + + datetime_select = select_year(datetime, options_with_prefix.call(1)) + datetime_select << select_month(datetime, options_with_prefix.call(2)) unless options[:discard_month] + datetime_select << select_day(datetime, options_with_prefix.call(3)) unless options[:discard_day] || options[:discard_month] + datetime_select << ' — ' + select_hour(datetime, options_with_prefix.call(4)) unless options[:discard_hour] + datetime_select << ' : ' + select_minute(datetime, options_with_prefix.call(5)) unless options[:discard_minute] || options[:discard_hour] + + datetime_select + end + end + + class FormBuilder + def date_select(method, options = {}) + @template.date_select(@object_name, method, options.merge(:object => @object)) + end + + def datetime_select(method, options = {}) + @template.datetime_select(@object_name, method, options.merge(:object => @object)) + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/debug_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/debug_helper.rb new file mode 100644 index 00000000..8baea6f4 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/debug_helper.rb @@ -0,0 +1,17 @@ +module ActionView + module Helpers + # Provides a set of methods for making it easier to locate problems. + module DebugHelper + # Returns a
    -tag set with the +object+ dumped by YAML. Very readable way to inspect an object.
    +      def debug(object)
    +        begin
    +          Marshal::dump(object)
    +          "
    #{h(object.to_yaml).gsub("  ", "  ")}
    " + rescue Object => e + # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback + "#{h(object.inspect)}" + end + end + end + end +end \ No newline at end of file diff --git a/vendor/rails/actionpack/lib/action_view/helpers/form_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/form_helper.rb new file mode 100644 index 00000000..7c8748d6 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/form_helper.rb @@ -0,0 +1,406 @@ +require 'cgi' +require File.dirname(__FILE__) + '/date_helper' +require File.dirname(__FILE__) + '/tag_helper' + +module ActionView + module Helpers + # Provides a set of methods for working with forms and especially forms related to objects assigned to the template. + # The following is an example of a complete form for a person object that works for both creates and updates built + # with all the form helpers. The @person object was assigned by an action on the controller: + #
    + # Name: + # <%= text_field "person", "name", "size" => 20 %> + # + # Password: + # <%= password_field "person", "password", "maxsize" => 20 %> + # + # Single?: + # <%= check_box "person", "single" %> + # + # Description: + # <%= text_area "person", "description", "cols" => 20 %> + # + # + #
    + # + # ...is compiled to: + # + #
    + # Name: + # + # + # Password: + # + # + # Single?: + # + # + # Description: + # + # + # + #
    + # + # If the object name contains square brackets the id for the object will be inserted. Example: + # + # <%= text_field "person[]", "name" %> + # + # ...becomes: + # + # + # + # If the helper is being used to generate a repetitive sequence of similar form elements, for example in a partial + # used by render_collection_of_partials, the "index" option may come in handy. Example: + # + # <%= text_field "person", "name", "index" => 1 %> + # + # becomes + # + # + # + # There's also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html, + # link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html + module FormHelper + # Creates a form and a scope around a specific model object, which is then used as a base for questioning about + # values for the fields. Examples: + # + # <% form_for :person, @person, :url => { :action => "update" } do |f| %> + # First name: <%= f.text_field :first_name %> + # Last name : <%= f.text_field :last_name %> + # Biography : <%= f.text_area :biography %> + # Admin? : <%= f.check_box :admin %> + # <% end %> + # + # Worth noting is that the form_for tag is called in a ERb evaluation block, not a ERb output block. So that's <% %>, + # not <%= %>. Also worth noting is that the form_for yields a form_builder object, in this example as f, which emulates + # the API for the stand-alone FormHelper methods, but without the object name. So instead of text_field :person, :name, + # you get away with f.text_field :name. + # + # That in itself is a modest increase in comfort. The big news is that form_for allows us to more easily escape the instance + # variable convention, so while the stand-alone approach would require text_field :person, :name, :object => person + # to work with local variables instead of instance ones, the form_for calls remain the same. You simply declare once with + # :person, person and all subsequent field calls save :person and :object => person. + # + # Also note that form_for doesn't create an exclusive scope. It's still possible to use both the stand-alone FormHelper methods + # and methods from FormTagHelper. Example: + # + # <% form_for :person, @person, :url => { :action => "update" } do |f| %> + # First name: <%= f.text_field :first_name %> + # Last name : <%= f.text_field :last_name %> + # Biography : <%= text_area :person, :biography %> + # Admin? : <%= check_box_tag "person[admin]", @person.company.admin? %> + # <% end %> + # + # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base. + # Like collection_select and datetime_select. + # + # Html attributes for the form tag can be given as :html => {...}. Example: + # + # <% form_for :person, @person, :html => {:id => 'person_form'} do |f| %> + # ... + # <% end %> + # + # You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers, + # then use your custom builder like so: + # + # <% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %> + # <%= f.text_field :first_name %> + # <%= f.text_field :last_name %> + # <%= text_area :person, :biography %> + # <%= check_box_tag "person[admin]", @person.company.admin? %> + # <% end %> + # + # In many cases you will want to wrap the above in another helper, such as: + # + # def labelled_form_for(name, object, options, &proc) + # form_for(name, object, options.merge(:builder => LabellingFormBuiler), &proc) + # end + # + def form_for(object_name, *args, &proc) + raise ArgumentError, "Missing block" unless block_given? + options = args.last.is_a?(Hash) ? args.pop : {} + concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}), proc.binding) + fields_for(object_name, *(args << options), &proc) + concat('', proc.binding) + end + + # Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes + # fields_for suitable for specifying additional model objects in the same form. Example: + # + # <% form_for :person, @person, :url => { :action => "update" } do |person_form| %> + # First name: <%= person_form.text_field :first_name %> + # Last name : <%= person_form.text_field :last_name %> + # + # <% fields_for :permission, @person.permission do |permission_fields| %> + # Admin? : <%= permission_fields.check_box :admin %> + # <% end %> + # <% end %> + # + # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base. + # Like collection_select and datetime_select. + def fields_for(object_name, *args, &proc) + raise ArgumentError, "Missing block" unless block_given? + options = args.last.is_a?(Hash) ? args.pop : {} + object = args.first + yield((options[:builder] || FormBuilder).new(object_name, object, self, options, proc)) + end + + # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. + # + # Examples (call, result): + # text_field("post", "title", "size" => 20) + # + def text_field(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("text", options) + end + + # Works just like text_field, but returns an input tag of the "password" type instead. + def password_field(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("password", options) + end + + # Works just like text_field, but returns an input tag of the "hidden" type instead. + def hidden_field(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("hidden", options) + end + + # Works just like text_field, but returns an input tag of the "file" type instead, which won't have a default value. + def file_field(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("file", options) + end + + # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+) + # on an object assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. + # + # Example (call, result): + # text_area("post", "body", "cols" => 20, "rows" => 40) + # + def text_area(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_text_area_tag(options) + end + + # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). It's intended that +method+ returns an integer and if that + # integer is above zero, then the checkbox is checked. Additional options on the input tag can be passed as a + # hash with +options+. The +checked_value+ defaults to 1 while the default +unchecked_value+ + # is set to 0 which is convenient for boolean values. Usually unchecked checkboxes don't post anything. + # We work around this problem by adding a hidden value with the same name as the checkbox. + # + # Example (call, result). Imagine that @post.validated? returns 1: + # check_box("post", "validated") + # + # + # + # Example (call, result). Imagine that @puppy.gooddog returns no: + # check_box("puppy", "gooddog", {}, "yes", "no") + # + # + def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0") + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value) + end + + # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the + # radio button will be checked. Additional options on the input tag can be passed as a + # hash with +options+. + # Example (call, result). Imagine that @post.category returns "rails": + # radio_button("post", "category", "rails") + # radio_button("post", "category", "java") + # + # + # + def radio_button(object_name, method, tag_value, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_radio_button_tag(tag_value, options) + end + end + + class InstanceTag #:nodoc: + include Helpers::TagHelper + + attr_reader :method_name, :object_name + + DEFAULT_FIELD_OPTIONS = { "size" => 30 }.freeze unless const_defined?(:DEFAULT_FIELD_OPTIONS) + DEFAULT_RADIO_OPTIONS = { }.freeze unless const_defined?(:DEFAULT_RADIO_OPTIONS) + DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }.freeze unless const_defined?(:DEFAULT_TEXT_AREA_OPTIONS) + DEFAULT_DATE_OPTIONS = { :discard_type => true }.freeze unless const_defined?(:DEFAULT_DATE_OPTIONS) + + def initialize(object_name, method_name, template_object, local_binding = nil, object = nil) + @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup + @template_object, @local_binding = template_object, local_binding + @object = object + if @object_name.sub!(/\[\]$/,"") + @auto_index = @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}").id_before_type_cast + end + end + + def to_input_field_tag(field_type, options = {}) + options = options.stringify_keys + options["size"] ||= options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"] + options = DEFAULT_FIELD_OPTIONS.merge(options) + if field_type == "hidden" + options.delete("size") + end + options["type"] = field_type + options["value"] ||= value_before_type_cast unless field_type == "file" + add_default_name_and_id(options) + tag("input", options) + end + + def to_radio_button_tag(tag_value, options = {}) + options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys) + options["type"] = "radio" + options["value"] = tag_value + options["checked"] = "checked" if value.to_s == tag_value.to_s + pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase + options["id"] = @auto_index ? + "#{@object_name}_#{@auto_index}_#{@method_name}_#{pretty_tag_value}" : + "#{@object_name}_#{@method_name}_#{pretty_tag_value}" + add_default_name_and_id(options) + tag("input", options) + end + + def to_text_area_tag(options = {}) + options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys) + add_default_name_and_id(options) + content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast), options) + end + + def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0") + options = options.stringify_keys + options["type"] = "checkbox" + options["value"] = checked_value + checked = case value + when TrueClass, FalseClass + value + when NilClass + false + when Integer + value != 0 + when String + value == checked_value + else + value.to_i != 0 + end + if checked || options["checked"] == "checked" + options["checked"] = "checked" + else + options.delete("checked") + end + add_default_name_and_id(options) + tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => unchecked_value) + end + + def to_date_tag() + defaults = DEFAULT_DATE_OPTIONS.dup + date = value || Date.today + options = Proc.new { |position| defaults.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") } + html_day_select(date, options.call(3)) + + html_month_select(date, options.call(2)) + + html_year_select(date, options.call(1)) + end + + def to_boolean_select_tag(options = {}) + options = options.stringify_keys + add_default_name_and_id(options) + tag_text = "" + end + + def to_content_tag(tag_name, options = {}) + content_tag(tag_name, value, options) + end + + def object + @object || @template_object.instance_variable_get("@#{@object_name}") + end + + def value + unless object.nil? + object.send(@method_name) + end + end + + def value_before_type_cast + unless object.nil? + object.respond_to?(@method_name + "_before_type_cast") ? + object.send(@method_name + "_before_type_cast") : + object.send(@method_name) + end + end + + private + def add_default_name_and_id(options) + if options.has_key?("index") + options["name"] ||= tag_name_with_index(options["index"]) + options["id"] ||= tag_id_with_index(options["index"]) + options.delete("index") + elsif @auto_index + options["name"] ||= tag_name_with_index(@auto_index) + options["id"] ||= tag_id_with_index(@auto_index) + else + options["name"] ||= tag_name + options["id"] ||= tag_id + end + end + + def tag_name + "#{@object_name}[#{@method_name}]" + end + + def tag_name_with_index(index) + "#{@object_name}[#{index}][#{@method_name}]" + end + + def tag_id + "#{@object_name}_#{@method_name}" + end + + def tag_id_with_index(index) + "#{@object_name}_#{index}_#{@method_name}" + end + end + + class FormBuilder #:nodoc: + # The methods which wrap a form helper call. + class_inheritable_accessor :field_helpers + self.field_helpers = (FormHelper.instance_methods - ['form_for']) + + attr_accessor :object_name, :object + + def initialize(object_name, object, template, options, proc) + @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc + end + + (field_helpers - %w(check_box radio_button)).each do |selector| + src = <<-end_src + def #{selector}(method, options = {}) + @template.send(#{selector.inspect}, @object_name, method, options.merge(:object => @object)) + end + end_src + class_eval src, __FILE__, __LINE__ + end + + def check_box(method, options = {}, checked_value = "1", unchecked_value = "0") + @template.check_box(@object_name, method, options.merge(:object => @object), checked_value, unchecked_value) + end + + def radio_button(method, tag_value, options = {}) + @template.radio_button(@object_name, method, tag_value, options.merge(:object => @object)) + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/form_options_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/form_options_helper.rb new file mode 100644 index 00000000..53b39305 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -0,0 +1,361 @@ +require 'cgi' +require 'erb' +require File.dirname(__FILE__) + '/form_helper' + +module ActionView + module Helpers + # Provides a number of methods for turning different kinds of containers into a set of option tags. + # == Options + # The collection_select, country_select, select, + # and time_zone_select methods take an options parameter, + # a hash. + # + # * :include_blank - set to true if the first option element of the select element is a blank. Useful if there is not a default value required for the select element. For example, + # + # select("post", "category", Post::CATEGORIES, {:include_blank => true}) + # + # could become: + # + # + # + # * :prompt - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string. + # + # Another common case is a select tag for an belongs_to-associated object. For example, + # + # select("post", "person_id", Person.find_all.collect {|p| [ p.name, p.id ] }) + # + # could become: + # + # + module FormOptionsHelper + include ERB::Util + + # Create a select tag and a series of contained option tags for the provided object and method. + # The option currently held by the object will be selected, provided that the object is available. + # See options_for_select for the required format of the choices parameter. + # + # Example with @post.person_id => 1: + # select("post", "person_id", Person.find_all.collect {|p| [ p.name, p.id ] }, { :include_blank => true }) + # + # could become: + # + # + # + # This can be used to provide a default set of options in the standard way: before rendering the create form, a + # new model instance is assigned the default options and bound to @model_name. Usually this model is not saved + # to the database. Instead, a second model object is created when the create request is received. + # This allows the user to submit a form page more than once with the expected results of creating multiple records. + # In addition, this allows a single partial to be used to generate form inputs for both edit and create forms. + # + # By default, post.person_id is the selected option. Specify :selected => value to use a different selection + # or :selected => nil to leave all options unselected. + def select(object, method, choices, options = {}, html_options = {}) + InstanceTag.new(object, method, self, nil, options.delete(:object)).to_select_tag(choices, options, html_options) + end + + # Return select and option tags for the given object and method using options_from_collection_for_select to generate the list of option tags. + def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {}) + InstanceTag.new(object, method, self, nil, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options) + end + + # Return select and option tags for the given object and method, using country_options_for_select to generate the list of option tags. + def country_select(object, method, priority_countries = nil, options = {}, html_options = {}) + InstanceTag.new(object, method, self, nil, options.delete(:object)).to_country_select_tag(priority_countries, options, html_options) + end + + # Return select and option tags for the given object and method, using + # #time_zone_options_for_select to generate the list of option tags. + # + # In addition to the :include_blank option documented above, + # this method also supports a :model option, which defaults + # to TimeZone. This may be used by users to specify a different time + # zone model object. (See #time_zone_options_for_select for more + # information.) + def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {}) + InstanceTag.new(object, method, self, nil, options.delete(:object)).to_time_zone_select_tag(priority_zones, options, html_options) + end + + # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container + # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and + # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values + # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +Selected+ + # may also be an array of values to be selected when using a multiple select. + # + # Examples (call, result): + # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]]) + # \n + # + # options_for_select([ "VISA", "MasterCard" ], "MasterCard") + # \n + # + # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40") + # \n + # + # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"]) + # \n\n + # + # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. + def options_for_select(container, selected = nil) + container = container.to_a if Hash === container + + options_for_select = container.inject([]) do |options, element| + if !element.is_a?(String) and element.respond_to?(:first) and element.respond_to?(:last) + is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element.last) : element.last == selected) ) + is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element.last) : element.last == selected) ) + if is_selected + options << "" + else + options << "" + end + else + is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element) : element == selected) ) + is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element) : element == selected) ) + options << ((is_selected) ? "" : "") + end + end + + options_for_select.join("\n") + end + + # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the + # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text. + # If +selected_value+ is specified, the element returning a match on +value_method+ will get the selected option tag. + # + # Example (call, result). Imagine a loop iterating over each +person+ in @project.people to generate an input tag: + # options_from_collection_for_select(@project.people, "id", "name") + # + # + # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. + def options_from_collection_for_select(collection, value_method, text_method, selected_value = nil) + options_for_select( + collection.inject([]) { |options, object| options << [ object.send(text_method), object.send(value_method) ] }, + selected_value + ) + end + + # Returns a string of option tags, like options_from_collection_for_select, but surrounds them with tags. + # + # An array of group objects are passed. Each group should return an array of options when calling group_method + # Each group should return its name when calling group_label_method. + # + # html_option_groups_from_collection(@continents, "countries", "continent_name", "country_id", "country_name", @selected_country.id) + # + # Could become: + # + # + # + # ... + # + # + # + # + # + # ... + # + # + # with objects of the following classes: + # class Continent + # def initialize(p_name, p_countries) @continent_name = p_name; @countries = p_countries; end + # def continent_name() @continent_name; end + # def countries() @countries; end + # end + # class Country + # def initialize(id, name) @id = id; @name = name end + # def country_id() @id; end + # def country_name() @name; end + # end + # + # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. + def option_groups_from_collection_for_select(collection, group_method, group_label_method, + option_key_method, option_value_method, selected_key = nil) + collection.inject("") do |options_for_select, group| + group_label_string = eval("group.#{group_label_method}") + options_for_select += "" + options_for_select += options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key) + options_for_select += '' + end + end + + # Returns a string of option tags for pretty much any country in the world. Supply a country name as +selected+ to + # have it marked as the selected option tag. You can also supply an array of countries as +priority_countries+, so + # that they will be listed above the rest of the (long) list. + # + # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. + def country_options_for_select(selected = nil, priority_countries = nil) + country_options = "" + + if priority_countries + country_options += options_for_select(priority_countries, selected) + country_options += "\n" + end + + if priority_countries && priority_countries.include?(selected) + country_options += options_for_select(COUNTRIES - priority_countries, selected) + else + country_options += options_for_select(COUNTRIES, selected) + end + + return country_options + end + + # Returns a string of option tags for pretty much any time zone in the + # world. Supply a TimeZone name as +selected+ to have it marked as the + # selected option tag. You can also supply an array of TimeZone objects + # as +priority_zones+, so that they will be listed above the rest of the + # (long) list. (You can use TimeZone.us_zones as a convenience for + # obtaining a list of the US time zones.) + # + # The +selected+ parameter must be either +nil+, or a string that names + # a TimeZone. + # + # By default, +model+ is the TimeZone constant (which can be obtained + # in ActiveRecord as a value object). The only requirement is that the + # +model+ parameter be an object that responds to #all, and returns + # an array of objects that represent time zones. + # + # NOTE: Only the option tags are returned, you have to wrap this call in + # a regular HTML select tag. + def time_zone_options_for_select(selected = nil, priority_zones = nil, model = TimeZone) + zone_options = "" + + zones = model.all + convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } } + + if priority_zones + zone_options += options_for_select(convert_zones[priority_zones], selected) + zone_options += "\n" + + zones = zones.reject { |z| priority_zones.include?( z ) } + end + + zone_options += options_for_select(convert_zones[zones], selected) + zone_options + end + + private + # All the countries included in the country_options output. + COUNTRIES = [ "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", + "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", + "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", + "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", + "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory", + "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burma", "Burundi", "Cambodia", + "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", + "Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia", + "Comoros", "Congo", "Congo, the Democratic Republic of the", "Cook Islands", + "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba", "Cyprus", "Czech Republic", "Denmark", + "Djibouti", "Dominica", "Dominican Republic", "East Timor", "Ecuador", "Egypt", + "El Salvador", "England", "Equatorial Guinea", "Eritrea", "Espana", "Estonia", + "Ethiopia", "Falkland Islands", "Faroe Islands", "Fiji", "Finland", "France", + "French Guiana", "French Polynesia", "French Southern Territories", "Gabon", "Gambia", + "Georgia", "Germany", "Ghana", "Gibraltar", "Great Britain", "Greece", "Greenland", + "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana", + "Haiti", "Heard and Mc Donald Islands", "Honduras", "Hong Kong", "Hungary", "Iceland", + "India", "Indonesia", "Ireland", "Israel", "Italy", "Iran", "Iraq", "Jamaica", "Japan", "Jordan", + "Kazakhstan", "Kenya", "Kiribati", "Korea, Republic of", "Korea (South)", "Kuwait", + "Kyrgyzstan", "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", + "Liberia", "Liechtenstein", "Lithuania", "Luxembourg", "Macau", "Macedonia", + "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", + "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", + "Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia", + "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", + "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", + "Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Ireland", + "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Panama", + "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland", + "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russia", "Rwanda", + "Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", + "Samoa (Independent)", "San Marino", "Sao Tome and Principe", "Saudi Arabia", + "Scotland", "Senegal", "Serbia and Montenegro", "Seychelles", "Sierra Leone", "Singapore", + "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", + "South Georgia and the South Sandwich Islands", "South Korea", "Spain", "Sri Lanka", + "St. Helena", "St. Pierre and Miquelon", "Suriname", "Svalbard and Jan Mayen Islands", + "Swaziland", "Sweden", "Switzerland", "Taiwan", "Tajikistan", "Tanzania", "Thailand", + "Togo", "Tokelau", "Tonga", "Trinidad", "Trinidad and Tobago", "Tunisia", "Turkey", + "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", + "United Arab Emirates", "United Kingdom", "United States", + "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", + "Vatican City State (Holy See)", "Venezuela", "Viet Nam", "Virgin Islands (British)", + "Virgin Islands (U.S.)", "Wales", "Wallis and Futuna Islands", "Western Sahara", + "Yemen", "Zambia", "Zimbabwe" ] unless const_defined?("COUNTRIES") + end + + class InstanceTag #:nodoc: + include FormOptionsHelper + + def to_select_tag(choices, options, html_options) + html_options = html_options.stringify_keys + add_default_name_and_id(html_options) + selected_value = options.has_key?(:selected) ? options[:selected] : value + content_tag("select", add_options(options_for_select(choices, selected_value), options, value), html_options) + end + + def to_collection_select_tag(collection, value_method, text_method, options, html_options) + html_options = html_options.stringify_keys + add_default_name_and_id(html_options) + content_tag( + "select", add_options(options_from_collection_for_select(collection, value_method, text_method, value), options, value), html_options + ) + end + + def to_country_select_tag(priority_countries, options, html_options) + html_options = html_options.stringify_keys + add_default_name_and_id(html_options) + content_tag("select", add_options(country_options_for_select(value, priority_countries), options, value), html_options) + end + + def to_time_zone_select_tag(priority_zones, options, html_options) + html_options = html_options.stringify_keys + add_default_name_and_id(html_options) + content_tag("select", + add_options( + time_zone_options_for_select(value, priority_zones, options[:model] || TimeZone), + options, value + ), html_options + ) + end + + private + def add_options(option_tags, options, value = nil) + option_tags = "\n" + option_tags if options[:include_blank] + + if value.blank? && options[:prompt] + ("\n") + option_tags + else + option_tags + end + end + end + + class FormBuilder + def select(method, choices, options = {}, html_options = {}) + @template.select(@object_name, method, choices, options.merge(:object => @object), html_options) + end + + def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) + @template.collection_select(@object_name, method, collection, value_method, text_method, options.merge(:object => @object), html_options) + end + + def country_select(method, priority_countries = nil, options = {}, html_options = {}) + @template.country_select(@object_name, method, priority_countries, options.merge(:object => @object), html_options) + end + + def time_zone_select(method, priority_zones = nil, options = {}, html_options = {}) + @template.time_zone_select(@object_name, method, priority_zones, options.merge(:object => @object), html_options) + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/form_tag_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/form_tag_helper.rb new file mode 100644 index 00000000..c7a5d1bb --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -0,0 +1,138 @@ +require 'cgi' +require File.dirname(__FILE__) + '/tag_helper' + +module ActionView + module Helpers + # Provides a number of methods for creating form tags that doesn't rely on conventions with an object assigned to the template like + # FormHelper does. With the FormTagHelper, you provide the names and values yourself. + # + # NOTE: The html options disabled, readonly, and multiple can all be treated as booleans. So specifying :disabled => true + # will give disabled="disabled". + module FormTagHelper + # Starts a form tag that points the action to an url configured with url_for_options just like + # ActionController::Base#url_for. The method for the form defaults to POST. + # + # Options: + # * :multipart - If set to true, the enctype is set to "multipart/form-data". + # * :method - The method to use when submitting the form, usually either "get" or "post". + def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &proc) + html_options = { "method" => "post" }.merge(options.stringify_keys) + html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart") + html_options["action"] = url_for(url_for_options, *parameters_for_url) + tag :form, html_options, true + end + + alias_method :start_form_tag, :form_tag + + # Outputs "" + def end_form_tag + "" + end + + # Creates a dropdown selection box, or if the :multiple option is set to true, a multiple + # choice selection box. + # + # Helpers::FormOptions can be used to create common select boxes such as countries, time zones, or + # associated records. + # + # option_tags is a string containing the option tags for the select box: + # # Outputs + # select_tag "people", "" + # + # Options: + # * :multiple - If set to true the selection will allow multiple choices. + def select_tag(name, option_tags = nil, options = {}) + content_tag :select, option_tags, { "name" => name, "id" => name }.update(options.stringify_keys) + end + + # Creates a standard text field. + # + # Options: + # * :disabled - If set to true, the user will not be able to use this input. + # * :size - The number of visible characters that will fit in the input. + # * :maxlength - The maximum number of characters that the browser will allow the user to enter. + # + # A hash of standard HTML options for the tag. + def text_field_tag(name, value = nil, options = {}) + tag :input, { "type" => "text", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys) + end + + # Creates a hidden field. + # + # Takes the same options as text_field_tag + def hidden_field_tag(name, value = nil, options = {}) + text_field_tag(name, value, options.stringify_keys.update("type" => "hidden")) + end + + # Creates a file upload field. + # + # If you are using file uploads then you will also need to set the multipart option for the form: + # <%= form_tag { :action => "post" }, { :multipart => true } %> + # <%= file_field_tag "file" %> + # <%= submit_tag %> + # <%= end_form_tag %> + # + # The specified URL will then be passed a File object containing the selected file, or if the field + # was left blank, a StringIO object. + def file_field_tag(name, options = {}) + text_field_tag(name, nil, options.update("type" => "file")) + end + + # Creates a password field. + # + # Takes the same options as text_field_tag + def password_field_tag(name = "password", value = nil, options = {}) + text_field_tag(name, value, options.update("type" => "password")) + end + + # Creates a text input area. + # + # Options: + # * :size - A string specifying the dimensions of the textarea. + # # Outputs + # <%= text_area_tag "body", nil, :size => "25x10" %> + def text_area_tag(name, content = nil, options = {}) + options.stringify_keys! + + if size = options.delete("size") + options["cols"], options["rows"] = size.split("x") + end + + content_tag :textarea, content, { "name" => name, "id" => name }.update(options.stringify_keys) + end + + # Creates a check box. + def check_box_tag(name, value = "1", checked = false, options = {}) + html_options = { "type" => "checkbox", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys) + html_options["checked"] = "checked" if checked + tag :input, html_options + end + + # Creates a radio button. + def radio_button_tag(name, value, checked = false, options = {}) + html_options = { "type" => "radio", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys) + html_options["checked"] = "checked" if checked + tag :input, html_options + end + + # Creates a submit button with the text value as the caption. If options contains a pair with the key of "disable_with", + # then the value will be used to rename a disabled version of the submit button. + def submit_tag(value = "Save changes", options = {}) + options.stringify_keys! + + if disable_with = options.delete("disable_with") + options["onclick"] = "this.disabled=true;this.value='#{disable_with}';this.form.submit();#{options["onclick"]}" + end + + tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys) + end + + # Displays an image which when clicked will submit the form. + # + # source is passed to AssetTagHelper#image_path + def image_submit_tag(source, options = {}) + tag :input, { "type" => "image", "src" => image_path(source) }.update(options.stringify_keys) + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/java_script_macros_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/java_script_macros_helper.rb new file mode 100644 index 00000000..2cb64c95 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/java_script_macros_helper.rb @@ -0,0 +1,220 @@ +require File.dirname(__FILE__) + '/tag_helper' + +module ActionView + module Helpers + # Provides a set of helpers for creating JavaScript macros that rely on and often bundle methods from JavaScriptHelper into + # larger units. These macros also rely on counterparts in the controller that provide them with their backing. The in-place + # editing relies on ActionController::Base.in_place_edit_for and the autocompletion relies on + # ActionController::Base.auto_complete_for. + module JavaScriptMacrosHelper + # Makes an HTML element specified by the DOM ID +field_id+ become an in-place + # editor of a property. + # + # A form is automatically created and displayed when the user clicks the element, + # something like this: + #
    + # + # + # cancel + #
    + # + # The form is serialized and sent to the server using an AJAX call, the action on + # the server should process the value and return the updated value in the body of + # the reponse. The element will automatically be updated with the changed value + # (as returned from the server). + # + # Required +options+ are: + # :url:: Specifies the url where the updated value should + # be sent after the user presses "ok". + # + # + # Addtional +options+ are: + # :rows:: Number of rows (more than 1 will use a TEXTAREA) + # :cols:: Number of characters the text input should span (works for both INPUT and TEXTAREA) + # :size:: Synonym for :cols when using a single line text input. + # :cancel_text:: The text on the cancel link. (default: "cancel") + # :save_text:: The text on the save link. (default: "ok") + # :loading_text:: The text to display when submitting to the server (default: "Saving...") + # :external_control:: The id of an external control used to enter edit mode. + # :load_text_url:: URL where initial value of editor (content) is retrieved. + # :options:: Pass through options to the AJAX call (see prototype's Ajax.Updater) + # :with:: JavaScript snippet that should return what is to be sent + # in the AJAX call, +form+ is an implicit parameter + # :script:: Instructs the in-place editor to evaluate the remote JavaScript response (default: false) + def in_place_editor(field_id, options = {}) + function = "new Ajax.InPlaceEditor(" + function << "'#{field_id}', " + function << "'#{url_for(options[:url])}'" + + js_options = {} + js_options['cancelText'] = %('#{options[:cancel_text]}') if options[:cancel_text] + js_options['okText'] = %('#{options[:save_text]}') if options[:save_text] + js_options['loadingText'] = %('#{options[:loading_text]}') if options[:loading_text] + js_options['rows'] = options[:rows] if options[:rows] + js_options['cols'] = options[:cols] if options[:cols] + js_options['size'] = options[:size] if options[:size] + js_options['externalControl'] = "'#{options[:external_control]}'" if options[:external_control] + js_options['loadTextURL'] = "'#{url_for(options[:load_text_url])}'" if options[:load_text_url] + js_options['ajaxOptions'] = options[:options] if options[:options] + js_options['evalScripts'] = options[:script] if options[:script] + js_options['callback'] = "function(form) { return #{options[:with]} }" if options[:with] + function << (', ' + options_for_javascript(js_options)) unless js_options.empty? + + function << ')' + + javascript_tag(function) + end + + # Renders the value of the specified object and method with in-place editing capabilities. + # + # See the RDoc on ActionController::InPlaceEditing to learn more about this. + def in_place_editor_field(object, method, tag_options = {}, in_place_editor_options = {}) + tag = ::ActionView::Helpers::InstanceTag.new(object, method, self) + tag_options = {:tag => "span", :id => "#{object}_#{method}_#{tag.object.id}_in_place_editor", :class => "in_place_editor_field"}.merge!(tag_options) + in_place_editor_options[:url] = in_place_editor_options[:url] || url_for({ :action => "set_#{object}_#{method}", :id => tag.object.id }) + tag.to_content_tag(tag_options.delete(:tag), tag_options) + + in_place_editor(tag_options[:id], in_place_editor_options) + end + + # Adds AJAX autocomplete functionality to the text input field with the + # DOM ID specified by +field_id+. + # + # This function expects that the called action returns a HTML
      list, + # or nothing if no entries should be displayed for autocompletion. + # + # You'll probably want to turn the browser's built-in autocompletion off, + # so be sure to include a autocomplete="off" attribute with your text + # input field. + # + # The autocompleter object is assigned to a Javascript variable named field_id_auto_completer. + # This object is useful if you for example want to trigger the auto-complete suggestions through + # other means than user input (for that specific case, call the activate method on that object). + # + # Required +options+ are: + # :url:: URL to call for autocompletion results + # in url_for format. + # + # Addtional +options+ are: + # :update:: Specifies the DOM ID of the element whose + # innerHTML should be updated with the autocomplete + # entries returned by the AJAX request. + # Defaults to field_id + '_auto_complete' + # :with:: A JavaScript expression specifying the + # parameters for the XMLHttpRequest. This defaults + # to 'fieldname=value'. + # :frequency:: Determines the time to wait after the last keystroke + # for the AJAX request to be initiated. + # :indicator:: Specifies the DOM ID of an element which will be + # displayed while autocomplete is running. + # :tokens:: A string or an array of strings containing + # separator tokens for tokenized incremental + # autocompletion. Example: :tokens => ',' would + # allow multiple autocompletion entries, separated + # by commas. + # :min_chars:: The minimum number of characters that should be + # in the input field before an Ajax call is made + # to the server. + # :on_hide:: A Javascript expression that is called when the + # autocompletion div is hidden. The expression + # should take two variables: element and update. + # Element is a DOM element for the field, update + # is a DOM element for the div from which the + # innerHTML is replaced. + # :on_show:: Like on_hide, only now the expression is called + # then the div is shown. + # :after_update_element:: A Javascript expression that is called when the + # user has selected one of the proposed values. + # The expression should take two variables: element and value. + # Element is a DOM element for the field, value + # is the value selected by the user. + # :select:: Pick the class of the element from which the value for + # insertion should be extracted. If this is not specified, + # the entire element is used. + def auto_complete_field(field_id, options = {}) + function = "var #{field_id}_auto_completer = new Ajax.Autocompleter(" + function << "'#{field_id}', " + function << "'" + (options[:update] || "#{field_id}_auto_complete") + "', " + function << "'#{url_for(options[:url])}'" + + js_options = {} + js_options[:tokens] = array_or_string_for_javascript(options[:tokens]) if options[:tokens] + js_options[:callback] = "function(element, value) { return #{options[:with]} }" if options[:with] + js_options[:indicator] = "'#{options[:indicator]}'" if options[:indicator] + js_options[:select] = "'#{options[:select]}'" if options[:select] + js_options[:frequency] = "#{options[:frequency]}" if options[:frequency] + + { :after_update_element => :afterUpdateElement, + :on_show => :onShow, :on_hide => :onHide, :min_chars => :minChars }.each do |k,v| + js_options[v] = options[k] if options[k] + end + + function << (', ' + options_for_javascript(js_options) + ')') + + javascript_tag(function) + end + + # Use this method in your view to generate a return for the AJAX autocomplete requests. + # + # Example action: + # + # def auto_complete_for_item_title + # @items = Item.find(:all, + # :conditions => [ 'LOWER(description) LIKE ?', + # '%' + request.raw_post.downcase + '%' ]) + # render :inline => '<%= auto_complete_result(@items, 'description') %>' + # end + # + # The auto_complete_result can of course also be called from a view belonging to the + # auto_complete action if you need to decorate it further. + def auto_complete_result(entries, field, phrase = nil) + return unless entries + items = entries.map { |entry| content_tag("li", phrase ? highlight(entry[field], phrase) : h(entry[field])) } + content_tag("ul", items.uniq) + end + + # Wrapper for text_field with added AJAX autocompletion functionality. + # + # In your controller, you'll need to define an action called + # auto_complete_for_object_method to respond the AJAX calls, + # + # See the RDoc on ActionController::AutoComplete to learn more about this. + def text_field_with_auto_complete(object, method, tag_options = {}, completion_options = {}) + (completion_options[:skip_style] ? "" : auto_complete_stylesheet) + + text_field(object, method, tag_options) + + content_tag("div", "", :id => "#{object}_#{method}_auto_complete", :class => "auto_complete") + + auto_complete_field("#{object}_#{method}", { :url => { :action => "auto_complete_for_#{object}_#{method}" } }.update(completion_options)) + end + + private + def auto_complete_stylesheet + content_tag("style", <<-EOT + div.auto_complete { + width: 350px; + background: #fff; + } + div.auto_complete ul { + border:1px solid #888; + margin:0; + padding:0; + width:100%; + list-style-type:none; + } + div.auto_complete ul li { + margin:0; + padding:3px; + } + div.auto_complete ul li.selected { + background-color: #ffb; + } + div.auto_complete ul strong.highlight { + color: #800; + margin:0; + padding:0; + } + EOT + ) + end + + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/javascript_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/javascript_helper.rb new file mode 100644 index 00000000..93a6e700 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -0,0 +1,132 @@ +require File.dirname(__FILE__) + '/tag_helper' + +module ActionView + module Helpers + # Provides functionality for working with JavaScript in your views. + # + # == Ajax, controls and visual effects + # + # * For information on using Ajax, see + # ActionView::Helpers::PrototypeHelper. + # * For information on using controls and visual effects, see + # ActionView::Helpers::ScriptaculousHelper. + # + # == Including the JavaScript libraries into your pages + # + # Rails includes the Prototype JavaScript framework and the Scriptaculous + # JavaScript controls and visual effects library. If you wish to use + # these libraries and their helpers (ActionView::Helpers::PrototypeHelper + # and ActionView::Helpers::ScriptaculousHelper), you must do one of the + # following: + # + # * Use <%= javascript_include_tag :defaults %> in the HEAD + # section of your page (recommended): This function will return + # references to the JavaScript files created by the +rails+ command in + # your public/javascripts directory. Using it is recommended as + # the browser can then cache the libraries instead of fetching all the + # functions anew on every request. + # * Use <%= javascript_include_tag 'prototype' %>: As above, but + # will only include the Prototype core library, which means you are able + # to use all basic AJAX functionality. For the Scriptaculous-based + # JavaScript helpers, like visual effects, autocompletion, drag and drop + # and so on, you should use the method described above. + # * Use <%= define_javascript_functions %>: this will copy all the + # JavaScript support functions within a single script block. Not + # recommended. + # + # For documentation on +javascript_include_tag+ see + # ActionView::Helpers::AssetTagHelper. + module JavaScriptHelper + unless const_defined? :JAVASCRIPT_PATH + JAVASCRIPT_PATH = File.join(File.dirname(__FILE__), 'javascripts') + end + + # Returns a link that'll trigger a JavaScript +function+ using the + # onclick handler and return false after the fact. + # + # Examples: + # link_to_function "Greeting", "alert('Hello world!')" + # link_to_function(image_tag("delete"), "if confirm('Really?'){ do_delete(); }") + def link_to_function(name, function, html_options = {}) + html_options.symbolize_keys! + content_tag( + "a", name, + html_options.merge({ + :href => html_options[:href] || "#", + :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function}; return false;" + }) + ) + end + + # Returns a link that'll trigger a JavaScript +function+ using the + # onclick handler. + # + # Examples: + # button_to_function "Greeting", "alert('Hello world!')" + # button_to_function "Delete", "if confirm('Really?'){ do_delete(); }") + def button_to_function(name, function, html_options = {}) + html_options.symbolize_keys! + tag(:input, html_options.merge({ + :type => "button", :value => name, + :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" + })) + end + + # Includes the Action Pack JavaScript libraries inside a single ' + end + + # Escape carrier returns and single and double quotes for JavaScript segments. + def escape_javascript(javascript) + (javascript || '').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" } + end + + # Returns a JavaScript tag with the +content+ inside. Example: + # javascript_tag "alert('All is good')" # => + def javascript_tag(content) + content_tag("script", javascript_cdata_section(content), :type => "text/javascript") + end + + def javascript_cdata_section(content) #:nodoc: + "\n//#{cdata_section("\n#{content}\n//")}\n" + end + + protected + def options_for_javascript(options) + '{' + options.map {|k, v| "#{k}:#{v}"}.sort.join(', ') + '}' + end + + def array_or_string_for_javascript(option) + js_option = if option.kind_of?(Array) + "['#{option.join('\',\'')}']" + elsif !option.nil? + "'#{option}'" + end + js_option + end + end + + JavascriptHelper = JavaScriptHelper unless const_defined? :JavascriptHelper + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/javascripts/controls.js b/vendor/rails/actionpack/lib/action_view/helpers/javascripts/controls.js new file mode 100644 index 00000000..de0261ed --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/javascripts/controls.js @@ -0,0 +1,815 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005 Jon Tirsen (http://www.tirsen.com) +// Contributors: +// Richard Livsey +// Rahul Bhargava +// Rob Wills +// +// See scriptaculous.js for full license. + +// Autocompleter.Base handles all the autocompletion functionality +// that's independent of the data source for autocompletion. This +// includes drawing the autocompletion menu, observing keyboard +// and mouse events, and similar. +// +// Specific autocompleters need to provide, at the very least, +// a getUpdatedChoices function that will be invoked every time +// the text inside the monitored textbox changes. This method +// should get the text for which to provide autocompletion by +// invoking this.getToken(), NOT by directly accessing +// this.element.value. This is to allow incremental tokenized +// autocompletion. Specific auto-completion logic (AJAX, etc) +// belongs in getUpdatedChoices. +// +// Tokenized incremental autocompletion is enabled automatically +// when an autocompleter is instantiated with the 'tokens' option +// in the options parameter, e.g.: +// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); +// will incrementally autocomplete with a comma as the token. +// Additionally, ',' in the above example can be replaced with +// a token array, e.g. { tokens: [',', '\n'] } which +// enables autocompletion on multiple tokens. This is most +// useful when one of the tokens is \n (a newline), as it +// allows smart autocompletion after linebreaks. + +var Autocompleter = {} +Autocompleter.Base = function() {}; +Autocompleter.Base.prototype = { + baseInitialize: function(element, update, options) { + this.element = $(element); + this.update = $(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entryCount = 0; + + if (this.setOptions) + this.setOptions(options); + else + this.options = options || {}; + + this.options.paramName = this.options.paramName || this.element.name; + this.options.tokens = this.options.tokens || []; + this.options.frequency = this.options.frequency || 0.4; + this.options.minChars = this.options.minChars || 1; + this.options.onShow = this.options.onShow || + function(element, update){ + if(!update.style.position || update.style.position=='absolute') { + update.style.position = 'absolute'; + Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight}); + } + Effect.Appear(update,{duration:0.15}); + }; + this.options.onHide = this.options.onHide || + function(element, update){ new Effect.Fade(update,{duration:0.15}) }; + + if (typeof(this.options.tokens) == 'string') + this.options.tokens = new Array(this.options.tokens); + + this.observer = null; + + this.element.setAttribute('autocomplete','off'); + + Element.hide(this.update); + + Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); + }, + + show: function() { + if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && + (navigator.appVersion.indexOf('MSIE')>0) && + (navigator.userAgent.indexOf('Opera')<0) && + (Element.getStyle(this.update, 'position')=='absolute')) { + new Insertion.After(this.update, + ''); + this.iefix = $(this.update.id+'_iefix'); + } + if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); + }, + + fixIEOverlapping: function() { + Position.clone(this.update, this.iefix); + this.iefix.style.zIndex = 1; + this.update.style.zIndex = 2; + Element.show(this.iefix); + }, + + hide: function() { + this.stopIndicator(); + if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); + if(this.iefix) Element.hide(this.iefix); + }, + + startIndicator: function() { + if(this.options.indicator) Element.show(this.options.indicator); + }, + + stopIndicator: function() { + if(this.options.indicator) Element.hide(this.options.indicator); + }, + + onKeyPress: function(event) { + if(this.active) + switch(event.keyCode) { + case Event.KEY_TAB: + case Event.KEY_RETURN: + this.selectEntry(); + Event.stop(event); + case Event.KEY_ESC: + this.hide(); + this.active = false; + Event.stop(event); + return; + case Event.KEY_LEFT: + case Event.KEY_RIGHT: + return; + case Event.KEY_UP: + this.markPrevious(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + case Event.KEY_DOWN: + this.markNext(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || + (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return; + + this.changed = true; + this.hasFocus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); + }, + + activate: function() { + this.changed = false; + this.hasFocus = true; + this.getUpdatedChoices(); + }, + + onHover: function(event) { + var element = Event.findElement(event, 'LI'); + if(this.index != element.autocompleteIndex) + { + this.index = element.autocompleteIndex; + this.render(); + } + Event.stop(event); + }, + + onClick: function(event) { + var element = Event.findElement(event, 'LI'); + this.index = element.autocompleteIndex; + this.selectEntry(); + this.hide(); + }, + + onBlur: function(event) { + // needed to make click events working + setTimeout(this.hide.bind(this), 250); + this.hasFocus = false; + this.active = false; + }, + + render: function() { + if(this.entryCount > 0) { + for (var i = 0; i < this.entryCount; i++) + this.index==i ? + Element.addClassName(this.getEntry(i),"selected") : + Element.removeClassName(this.getEntry(i),"selected"); + + if(this.hasFocus) { + this.show(); + this.active = true; + } + } else { + this.active = false; + this.hide(); + } + }, + + markPrevious: function() { + if(this.index > 0) this.index-- + else this.index = this.entryCount-1; + }, + + markNext: function() { + if(this.index < this.entryCount-1) this.index++ + else this.index = 0; + }, + + getEntry: function(index) { + return this.update.firstChild.childNodes[index]; + }, + + getCurrentEntry: function() { + return this.getEntry(this.index); + }, + + selectEntry: function() { + this.active = false; + this.updateElement(this.getCurrentEntry()); + }, + + updateElement: function(selectedElement) { + if (this.options.updateElement) { + this.options.updateElement(selectedElement); + return; + } + var value = ''; + if (this.options.select) { + var nodes = document.getElementsByClassName(this.options.select, selectedElement) || []; + if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); + } else + value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + + var lastTokenPos = this.findLastToken(); + if (lastTokenPos != -1) { + var newValue = this.element.value.substr(0, lastTokenPos + 1); + var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/); + if (whitespace) + newValue += whitespace[0]; + this.element.value = newValue + value; + } else { + this.element.value = value; + } + this.element.focus(); + + if (this.options.afterUpdateElement) + this.options.afterUpdateElement(this.element, selectedElement); + }, + + updateChoices: function(choices) { + if(!this.changed && this.hasFocus) { + this.update.innerHTML = choices; + Element.cleanWhitespace(this.update); + Element.cleanWhitespace(this.update.firstChild); + + if(this.update.firstChild && this.update.firstChild.childNodes) { + this.entryCount = + this.update.firstChild.childNodes.length; + for (var i = 0; i < this.entryCount; i++) { + var entry = this.getEntry(i); + entry.autocompleteIndex = i; + this.addObservers(entry); + } + } else { + this.entryCount = 0; + } + + this.stopIndicator(); + + this.index = 0; + this.render(); + } + }, + + addObservers: function(element) { + Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); + Event.observe(element, "click", this.onClick.bindAsEventListener(this)); + }, + + onObserverEvent: function() { + this.changed = false; + if(this.getToken().length>=this.options.minChars) { + this.startIndicator(); + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + }, + + getToken: function() { + var tokenPos = this.findLastToken(); + if (tokenPos != -1) + var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,''); + else + var ret = this.element.value; + + return /\n/.test(ret) ? '' : ret; + }, + + findLastToken: function() { + var lastTokenPos = -1; + + for (var i=0; i lastTokenPos) + lastTokenPos = thisTokenPos; + } + return lastTokenPos; + } +} + +Ajax.Autocompleter = Class.create(); +Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), { + initialize: function(element, update, url, options) { + this.baseInitialize(element, update, options); + this.options.asynchronous = true; + this.options.onComplete = this.onComplete.bind(this); + this.options.defaultParams = this.options.parameters || null; + this.url = url; + }, + + getUpdatedChoices: function() { + entry = encodeURIComponent(this.options.paramName) + '=' + + encodeURIComponent(this.getToken()); + + this.options.parameters = this.options.callback ? + this.options.callback(this.element, entry) : entry; + + if(this.options.defaultParams) + this.options.parameters += '&' + this.options.defaultParams; + + new Ajax.Request(this.url, this.options); + }, + + onComplete: function(request) { + this.updateChoices(request.responseText); + } + +}); + +// The local array autocompleter. Used when you'd prefer to +// inject an array of autocompletion options into the page, rather +// than sending out Ajax queries, which can be quite slow sometimes. +// +// The constructor takes four parameters. The first two are, as usual, +// the id of the monitored textbox, and id of the autocompletion menu. +// The third is the array you want to autocomplete from, and the fourth +// is the options block. +// +// Extra local autocompletion options: +// - choices - How many autocompletion choices to offer +// +// - partialSearch - If false, the autocompleter will match entered +// text only at the beginning of strings in the +// autocomplete array. Defaults to true, which will +// match text at the beginning of any *word* in the +// strings in the autocomplete array. If you want to +// search anywhere in the string, additionally set +// the option fullSearch to true (default: off). +// +// - fullSsearch - Search anywhere in autocomplete array strings. +// +// - partialChars - How many characters to enter before triggering +// a partial match (unlike minChars, which defines +// how many characters are required to do any match +// at all). Defaults to 2. +// +// - ignoreCase - Whether to ignore case when autocompleting. +// Defaults to true. +// +// It's possible to pass in a custom function as the 'selector' +// option, if you prefer to write your own autocompletion logic. +// In that case, the other options above will not apply unless +// you support them. + +Autocompleter.Local = Class.create(); +Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { + initialize: function(element, update, array, options) { + this.baseInitialize(element, update, options); + this.options.array = array; + }, + + getUpdatedChoices: function() { + this.updateChoices(this.options.selector(this)); + }, + + setOptions: function(options) { + this.options = Object.extend({ + choices: 10, + partialSearch: true, + partialChars: 2, + ignoreCase: true, + fullSearch: false, + selector: function(instance) { + var ret = []; // Beginning matches + var partial = []; // Inside matches + var entry = instance.getToken(); + var count = 0; + + for (var i = 0; i < instance.options.array.length && + ret.length < instance.options.choices ; i++) { + + var elem = instance.options.array[i]; + var foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : + elem.indexOf(entry); + + while (foundPos != -1) { + if (foundPos == 0 && elem.length != entry.length) { + ret.push("
    • " + elem.substr(0, entry.length) + "" + + elem.substr(entry.length) + "
    • "); + break; + } else if (entry.length >= instance.options.partialChars && + instance.options.partialSearch && foundPos != -1) { + if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { + partial.push("
    • " + elem.substr(0, foundPos) + "" + + elem.substr(foundPos, entry.length) + "" + elem.substr( + foundPos + entry.length) + "
    • "); + break; + } + } + + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + elem.indexOf(entry, foundPos + 1); + + } + } + if (partial.length) + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) + return "
        " + ret.join('') + "
      "; + } + }, options || {}); + } +}); + +// AJAX in-place editor +// +// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor + +// Use this if you notice weird scrolling problems on some browsers, +// the DOM might be a bit confused when this gets called so do this +// waits 1 ms (with setTimeout) until it does the activation +Field.scrollFreeActivate = function(field) { + setTimeout(function() { + Field.activate(field); + }, 1); +} + +Ajax.InPlaceEditor = Class.create(); +Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99"; +Ajax.InPlaceEditor.prototype = { + initialize: function(element, url, options) { + this.url = url; + this.element = $(element); + + this.options = Object.extend({ + okButton: true, + okText: "ok", + cancelLink: true, + cancelText: "cancel", + savingText: "Saving...", + clickToEditText: "Click to edit", + okText: "ok", + rows: 1, + onComplete: function(transport, element) { + new Effect.Highlight(element, {startcolor: this.options.highlightcolor}); + }, + onFailure: function(transport) { + alert("Error communicating with the server: " + transport.responseText.stripTags()); + }, + callback: function(form) { + return Form.serialize(form); + }, + handleLineBreaks: true, + loadingText: 'Loading...', + savingClassName: 'inplaceeditor-saving', + loadingClassName: 'inplaceeditor-loading', + formClassName: 'inplaceeditor-form', + highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, + highlightendcolor: "#FFFFFF", + externalControl: null, + submitOnBlur: false, + ajaxOptions: {}, + evalScripts: false + }, options || {}); + + if(!this.options.formId && this.element.id) { + this.options.formId = this.element.id + "-inplaceeditor"; + if ($(this.options.formId)) { + // there's already a form with that name, don't specify an id + this.options.formId = null; + } + } + + if (this.options.externalControl) { + this.options.externalControl = $(this.options.externalControl); + } + + this.originalBackground = Element.getStyle(this.element, 'background-color'); + if (!this.originalBackground) { + this.originalBackground = "transparent"; + } + + this.element.title = this.options.clickToEditText; + + this.onclickListener = this.enterEditMode.bindAsEventListener(this); + this.mouseoverListener = this.enterHover.bindAsEventListener(this); + this.mouseoutListener = this.leaveHover.bindAsEventListener(this); + Event.observe(this.element, 'click', this.onclickListener); + Event.observe(this.element, 'mouseover', this.mouseoverListener); + Event.observe(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.observe(this.options.externalControl, 'click', this.onclickListener); + Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + }, + enterEditMode: function(evt) { + if (this.saving) return; + if (this.editing) return; + this.editing = true; + this.onEnterEditMode(); + if (this.options.externalControl) { + Element.hide(this.options.externalControl); + } + Element.hide(this.element); + this.createForm(); + this.element.parentNode.insertBefore(this.form, this.element); + Field.scrollFreeActivate(this.editField); + // stop the event to avoid a page refresh in Safari + if (evt) { + Event.stop(evt); + } + return false; + }, + createForm: function() { + this.form = document.createElement("form"); + this.form.id = this.options.formId; + Element.addClassName(this.form, this.options.formClassName) + this.form.onsubmit = this.onSubmit.bind(this); + + this.createEditField(); + + if (this.options.textarea) { + var br = document.createElement("br"); + this.form.appendChild(br); + } + + if (this.options.okButton) { + okButton = document.createElement("input"); + okButton.type = "submit"; + okButton.value = this.options.okText; + okButton.className = 'editor_ok_button'; + this.form.appendChild(okButton); + } + + if (this.options.cancelLink) { + cancelLink = document.createElement("a"); + cancelLink.href = "#"; + cancelLink.appendChild(document.createTextNode(this.options.cancelText)); + cancelLink.onclick = this.onclickCancel.bind(this); + cancelLink.className = 'editor_cancel'; + this.form.appendChild(cancelLink); + } + }, + hasHTMLLineBreaks: function(string) { + if (!this.options.handleLineBreaks) return false; + return string.match(/
      /i); + }, + convertHTMLLineBreaks: function(string) { + return string.replace(/
      /gi, "\n").replace(//gi, "\n").replace(/<\/p>/gi, "\n").replace(/

      /gi, ""); + }, + createEditField: function() { + var text; + if(this.options.loadTextURL) { + text = this.options.loadingText; + } else { + text = this.getText(); + } + + var obj = this; + + if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { + this.options.textarea = false; + var textField = document.createElement("input"); + textField.obj = this; + textField.type = "text"; + textField.name = "value"; + textField.value = text; + textField.style.backgroundColor = this.options.highlightcolor; + textField.className = 'editor_field'; + var size = this.options.size || this.options.cols || 0; + if (size != 0) textField.size = size; + if (this.options.submitOnBlur) + textField.onblur = this.onSubmit.bind(this); + this.editField = textField; + } else { + this.options.textarea = true; + var textArea = document.createElement("textarea"); + textArea.obj = this; + textArea.name = "value"; + textArea.value = this.convertHTMLLineBreaks(text); + textArea.rows = this.options.rows; + textArea.cols = this.options.cols || 40; + textArea.className = 'editor_field'; + if (this.options.submitOnBlur) + textArea.onblur = this.onSubmit.bind(this); + this.editField = textArea; + } + + if(this.options.loadTextURL) { + this.loadExternalText(); + } + this.form.appendChild(this.editField); + }, + getText: function() { + return this.element.innerHTML; + }, + loadExternalText: function() { + Element.addClassName(this.form, this.options.loadingClassName); + this.editField.disabled = true; + new Ajax.Request( + this.options.loadTextURL, + Object.extend({ + asynchronous: true, + onComplete: this.onLoadedExternalText.bind(this) + }, this.options.ajaxOptions) + ); + }, + onLoadedExternalText: function(transport) { + Element.removeClassName(this.form, this.options.loadingClassName); + this.editField.disabled = false; + this.editField.value = transport.responseText.stripTags(); + }, + onclickCancel: function() { + this.onComplete(); + this.leaveEditMode(); + return false; + }, + onFailure: function(transport) { + this.options.onFailure(transport); + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + this.oldInnerHTML = null; + } + return false; + }, + onSubmit: function() { + // onLoading resets these so we need to save them away for the Ajax call + var form = this.form; + var value = this.editField.value; + + // do this first, sometimes the ajax call returns before we get a chance to switch on Saving... + // which means this will actually switch on Saving... *after* we've left edit mode causing Saving... + // to be displayed indefinitely + this.onLoading(); + + if (this.options.evalScripts) { + new Ajax.Request( + this.url, Object.extend({ + parameters: this.options.callback(form, value), + onComplete: this.onComplete.bind(this), + onFailure: this.onFailure.bind(this), + asynchronous:true, + evalScripts:true + }, this.options.ajaxOptions)); + } else { + new Ajax.Updater( + { success: this.element, + // don't update on failure (this could be an option) + failure: null }, + this.url, Object.extend({ + parameters: this.options.callback(form, value), + onComplete: this.onComplete.bind(this), + onFailure: this.onFailure.bind(this) + }, this.options.ajaxOptions)); + } + // stop the event to avoid a page refresh in Safari + if (arguments.length > 1) { + Event.stop(arguments[0]); + } + return false; + }, + onLoading: function() { + this.saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + showSaving: function() { + this.oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + Element.addClassName(this.element, this.options.savingClassName); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + }, + removeForm: function() { + if(this.form) { + if (this.form.parentNode) Element.remove(this.form); + this.form = null; + } + }, + enterHover: function() { + if (this.saving) return; + this.element.style.backgroundColor = this.options.highlightcolor; + if (this.effect) { + this.effect.cancel(); + } + Element.addClassName(this.element, this.options.hoverClassName) + }, + leaveHover: function() { + if (this.options.backgroundColor) { + this.element.style.backgroundColor = this.oldBackground; + } + Element.removeClassName(this.element, this.options.hoverClassName) + if (this.saving) return; + this.effect = new Effect.Highlight(this.element, { + startcolor: this.options.highlightcolor, + endcolor: this.options.highlightendcolor, + restorecolor: this.originalBackground + }); + }, + leaveEditMode: function() { + Element.removeClassName(this.element, this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + if (this.options.externalControl) { + Element.show(this.options.externalControl); + } + this.editing = false; + this.saving = false; + this.oldInnerHTML = null; + this.onLeaveEditMode(); + }, + onComplete: function(transport) { + this.leaveEditMode(); + this.options.onComplete.bind(this)(transport, this.element); + }, + onEnterEditMode: function() {}, + onLeaveEditMode: function() {}, + dispose: function() { + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + } + this.leaveEditMode(); + Event.stopObserving(this.element, 'click', this.onclickListener); + Event.stopObserving(this.element, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.stopObserving(this.options.externalControl, 'click', this.onclickListener); + Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + } +}; + +Ajax.InPlaceCollectionEditor = Class.create(); +Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype); +Object.extend(Ajax.InPlaceCollectionEditor.prototype, { + createEditField: function() { + if (!this.cached_selectTag) { + var selectTag = document.createElement("select"); + var collection = this.options.collection || []; + var optionTag; + collection.each(function(e,i) { + optionTag = document.createElement("option"); + optionTag.value = (e instanceof Array) ? e[0] : e; + if(this.options.value==optionTag.value) optionTag.selected = true; + optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e)); + selectTag.appendChild(optionTag); + }.bind(this)); + this.cached_selectTag = selectTag; + } + + this.editField = this.cached_selectTag; + if(this.options.loadTextURL) this.loadExternalText(); + this.form.appendChild(this.editField); + this.options.callback = function(form, value) { + return "value=" + encodeURIComponent(value); + } + } +}); + +// Delayed observer, like Form.Element.Observer, +// but waits for delay after last key input +// Ideal for live-search fields + +Form.Element.DelayedObserver = Class.create(); +Form.Element.DelayedObserver.prototype = { + initialize: function(element, delay, callback) { + this.delay = delay || 0.5; + this.element = $(element); + this.callback = callback; + this.timer = null; + this.lastValue = $F(this.element); + Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); + }, + delayedListener: function(event) { + if(this.lastValue == $F(this.element)) return; + if(this.timer) clearTimeout(this.timer); + this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); + this.lastValue = $F(this.element); + }, + onTimerEvent: function() { + this.timer = null; + this.callback(this.element, $F(this.element)); + } +}; diff --git a/vendor/rails/actionpack/lib/action_view/helpers/javascripts/dragdrop.js b/vendor/rails/actionpack/lib/action_view/helpers/javascripts/dragdrop.js new file mode 100644 index 00000000..a01b7be2 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/javascripts/dragdrop.js @@ -0,0 +1,913 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) +// +// See scriptaculous.js for full license. + +/*--------------------------------------------------------------------------*/ + +var Droppables = { + drops: [], + + remove: function(element) { + this.drops = this.drops.reject(function(d) { return d.element==$(element) }); + }, + + add: function(element) { + element = $(element); + var options = Object.extend({ + greedy: true, + hoverclass: null, + tree: false + }, arguments[1] || {}); + + // cache containers + if(options.containment) { + options._containers = []; + var containment = options.containment; + if((typeof containment == 'object') && + (containment.constructor == Array)) { + containment.each( function(c) { options._containers.push($(c)) }); + } else { + options._containers.push($(containment)); + } + } + + if(options.accept) options.accept = [options.accept].flatten(); + + Element.makePositioned(element); // fix IE + options.element = element; + + this.drops.push(options); + }, + + findDeepestChild: function(drops) { + deepest = drops[0]; + + for (i = 1; i < drops.length; ++i) + if (Element.isParent(drops[i].element, deepest.element)) + deepest = drops[i]; + + return deepest; + }, + + isContained: function(element, drop) { + var containmentNode; + if(drop.tree) { + containmentNode = element.treeNode; + } else { + containmentNode = element.parentNode; + } + return drop._containers.detect(function(c) { return containmentNode == c }); + }, + + isAffected: function(point, element, drop) { + return ( + (drop.element!=element) && + ((!drop._containers) || + this.isContained(element, drop)) && + ((!drop.accept) || + (Element.classNames(element).detect( + function(v) { return drop.accept.include(v) } ) )) && + Position.within(drop.element, point[0], point[1]) ); + }, + + deactivate: function(drop) { + if(drop.hoverclass) + Element.removeClassName(drop.element, drop.hoverclass); + this.last_active = null; + }, + + activate: function(drop) { + if(drop.hoverclass) + Element.addClassName(drop.element, drop.hoverclass); + this.last_active = drop; + }, + + show: function(point, element) { + if(!this.drops.length) return; + var affected = []; + + if(this.last_active) this.deactivate(this.last_active); + this.drops.each( function(drop) { + if(Droppables.isAffected(point, element, drop)) + affected.push(drop); + }); + + if(affected.length>0) { + drop = Droppables.findDeepestChild(affected); + Position.within(drop.element, point[0], point[1]); + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + + Droppables.activate(drop); + } + }, + + fire: function(event, element) { + if(!this.last_active) return; + Position.prepare(); + + if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) + if (this.last_active.onDrop) + this.last_active.onDrop(element, this.last_active.element, event); + }, + + reset: function() { + if(this.last_active) + this.deactivate(this.last_active); + } +} + +var Draggables = { + drags: [], + observers: [], + + register: function(draggable) { + if(this.drags.length == 0) { + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.updateDrag.bindAsEventListener(this); + this.eventKeypress = this.keyPress.bindAsEventListener(this); + + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + Event.observe(document, "keypress", this.eventKeypress); + } + this.drags.push(draggable); + }, + + unregister: function(draggable) { + this.drags = this.drags.reject(function(d) { return d==draggable }); + if(this.drags.length == 0) { + Event.stopObserving(document, "mouseup", this.eventMouseUp); + Event.stopObserving(document, "mousemove", this.eventMouseMove); + Event.stopObserving(document, "keypress", this.eventKeypress); + } + }, + + activate: function(draggable) { + window.focus(); // allows keypress events if window isn't currently focused, fails for Safari + this.activeDraggable = draggable; + }, + + deactivate: function() { + this.activeDraggable = null; + }, + + updateDrag: function(event) { + if(!this.activeDraggable) return; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + // Mozilla-based browsers fire successive mousemove events with + // the same coordinates, prevent needless redrawing (moz bug?) + if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; + this._lastPointer = pointer; + this.activeDraggable.updateDrag(event, pointer); + }, + + endDrag: function(event) { + if(!this.activeDraggable) return; + this._lastPointer = null; + this.activeDraggable.endDrag(event); + this.activeDraggable = null; + }, + + keyPress: function(event) { + if(this.activeDraggable) + this.activeDraggable.keyPress(event); + }, + + addObserver: function(observer) { + this.observers.push(observer); + this._cacheObserverCallbacks(); + }, + + removeObserver: function(element) { // element instead of observer fixes mem leaks + this.observers = this.observers.reject( function(o) { return o.element==element }); + this._cacheObserverCallbacks(); + }, + + notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' + if(this[eventName+'Count'] > 0) + this.observers.each( function(o) { + if(o[eventName]) o[eventName](eventName, draggable, event); + }); + }, + + _cacheObserverCallbacks: function() { + ['onStart','onEnd','onDrag'].each( function(eventName) { + Draggables[eventName+'Count'] = Draggables.observers.select( + function(o) { return o[eventName]; } + ).length; + }); + } +} + +/*--------------------------------------------------------------------------*/ + +var Draggable = Class.create(); +Draggable.prototype = { + initialize: function(element) { + var options = Object.extend({ + handle: false, + starteffect: function(element) { + new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); + }, + reverteffect: function(element, top_offset, left_offset) { + var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; + element._revert = new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur}); + }, + endeffect: function(element) { + new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); + }, + zindex: 1000, + revert: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] } + }, arguments[1] || {}); + + this.element = $(element); + + if(options.handle && (typeof options.handle == 'string')) { + var h = Element.childrenWithClassName(this.element, options.handle, true); + if(h.length>0) this.handle = h[0]; + } + if(!this.handle) this.handle = $(options.handle); + if(!this.handle) this.handle = this.element; + + if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) + options.scroll = $(options.scroll); + + Element.makePositioned(this.element); // fix IE + + this.delta = this.currentDelta(); + this.options = options; + this.dragging = false; + + this.eventMouseDown = this.initDrag.bindAsEventListener(this); + Event.observe(this.handle, "mousedown", this.eventMouseDown); + + Draggables.register(this); + }, + + destroy: function() { + Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); + Draggables.unregister(this); + }, + + currentDelta: function() { + return([ + parseInt(Element.getStyle(this.element,'left') || '0'), + parseInt(Element.getStyle(this.element,'top') || '0')]); + }, + + initDrag: function(event) { + if(Event.isLeftClick(event)) { + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if(src.tagName && ( + src.tagName=='INPUT' || + src.tagName=='SELECT' || + src.tagName=='OPTION' || + src.tagName=='BUTTON' || + src.tagName=='TEXTAREA')) return; + + if(this.element._revert) { + this.element._revert.cancel(); + this.element._revert = null; + } + + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var pos = Position.cumulativeOffset(this.element); + this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); + + Draggables.activate(this); + Event.stop(event); + } + }, + + startDrag: function(event) { + this.dragging = true; + + if(this.options.zindex) { + this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); + this.element.style.zIndex = this.options.zindex; + } + + if(this.options.ghosting) { + this._clone = this.element.cloneNode(true); + Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + + if(this.options.scroll) { + if (this.options.scroll == window) { + var where = this._getWindowScroll(this.options.scroll); + this.originalScrollLeft = where.left; + this.originalScrollTop = where.top; + } else { + this.originalScrollLeft = this.options.scroll.scrollLeft; + this.originalScrollTop = this.options.scroll.scrollTop; + } + } + + Draggables.notify('onStart', this, event); + if(this.options.starteffect) this.options.starteffect(this.element); + }, + + updateDrag: function(event, pointer) { + if(!this.dragging) this.startDrag(event); + Position.prepare(); + Droppables.show(pointer, this.element); + Draggables.notify('onDrag', this, event); + this.draw(pointer); + if(this.options.change) this.options.change(this); + + if(this.options.scroll) { + this.stopScrolling(); + + var p; + if (this.options.scroll == window) { + with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } + } else { + p = Position.page(this.options.scroll); + p[0] += this.options.scroll.scrollLeft; + p[1] += this.options.scroll.scrollTop; + p.push(p[0]+this.options.scroll.offsetWidth); + p.push(p[1]+this.options.scroll.offsetHeight); + } + var speed = [0,0]; + if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); + if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); + if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); + if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); + this.startScrolling(speed); + } + + // fix AppleWebKit rendering + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + + Event.stop(event); + }, + + finishDrag: function(event, success) { + this.dragging = false; + + if(this.options.ghosting) { + Position.relativize(this.element); + Element.remove(this._clone); + this._clone = null; + } + + if(success) Droppables.fire(event, this.element); + Draggables.notify('onEnd', this, event); + + var revert = this.options.revert; + if(revert && typeof revert == 'function') revert = revert(this.element); + + var d = this.currentDelta(); + if(revert && this.options.reverteffect) { + this.options.reverteffect(this.element, + d[1]-this.delta[1], d[0]-this.delta[0]); + } else { + this.delta = d; + } + + if(this.options.zindex) + this.element.style.zIndex = this.originalZ; + + if(this.options.endeffect) + this.options.endeffect(this.element); + + Draggables.deactivate(this); + Droppables.reset(); + }, + + keyPress: function(event) { + if(event.keyCode!=Event.KEY_ESC) return; + this.finishDrag(event, false); + Event.stop(event); + }, + + endDrag: function(event) { + if(!this.dragging) return; + this.stopScrolling(); + this.finishDrag(event, true); + Event.stop(event); + }, + + draw: function(point) { + var pos = Position.cumulativeOffset(this.element); + var d = this.currentDelta(); + pos[0] -= d[0]; pos[1] -= d[1]; + + if(this.options.scroll && (this.options.scroll != window)) { + pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; + pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; + } + + var p = [0,1].map(function(i){ + return (point[i]-pos[i]-this.offset[i]) + }.bind(this)); + + if(this.options.snap) { + if(typeof this.options.snap == 'function') { + p = this.options.snap(p[0],p[1]); + } else { + if(this.options.snap instanceof Array) { + p = p.map( function(v, i) { + return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this)) + } else { + p = p.map( function(v) { + return Math.round(v/this.options.snap)*this.options.snap }.bind(this)) + } + }} + + var style = this.element.style; + if((!this.options.constraint) || (this.options.constraint=='horizontal')) + style.left = p[0] + "px"; + if((!this.options.constraint) || (this.options.constraint=='vertical')) + style.top = p[1] + "px"; + if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering + }, + + stopScrolling: function() { + if(this.scrollInterval) { + clearInterval(this.scrollInterval); + this.scrollInterval = null; + Draggables._lastScrollPointer = null; + } + }, + + startScrolling: function(speed) { + this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; + this.lastScrolled = new Date(); + this.scrollInterval = setInterval(this.scroll.bind(this), 10); + }, + + scroll: function() { + var current = new Date(); + var delta = current - this.lastScrolled; + this.lastScrolled = current; + if(this.options.scroll == window) { + with (this._getWindowScroll(this.options.scroll)) { + if (this.scrollSpeed[0] || this.scrollSpeed[1]) { + var d = delta / 1000; + this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); + } + } + } else { + this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; + this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; + } + + Position.prepare(); + Droppables.show(Draggables._lastPointer, this.element); + Draggables.notify('onDrag', this); + Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); + Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; + Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; + if (Draggables._lastScrollPointer[0] < 0) + Draggables._lastScrollPointer[0] = 0; + if (Draggables._lastScrollPointer[1] < 0) + Draggables._lastScrollPointer[1] = 0; + this.draw(Draggables._lastScrollPointer); + + if(this.options.change) this.options.change(this); + }, + + _getWindowScroll: function(w) { + var T, L, W, H; + with (w.document) { + if (w.document.documentElement && documentElement.scrollTop) { + T = documentElement.scrollTop; + L = documentElement.scrollLeft; + } else if (w.document.body) { + T = body.scrollTop; + L = body.scrollLeft; + } + if (w.innerWidth) { + W = w.innerWidth; + H = w.innerHeight; + } else if (w.document.documentElement && documentElement.clientWidth) { + W = documentElement.clientWidth; + H = documentElement.clientHeight; + } else { + W = body.offsetWidth; + H = body.offsetHeight + } + } + return { top: T, left: L, width: W, height: H }; + } +} + +/*--------------------------------------------------------------------------*/ + +var SortableObserver = Class.create(); +SortableObserver.prototype = { + initialize: function(element, observer) { + this.element = $(element); + this.observer = observer; + this.lastValue = Sortable.serialize(this.element); + }, + + onStart: function() { + this.lastValue = Sortable.serialize(this.element); + }, + + onEnd: function() { + Sortable.unmark(); + if(this.lastValue != Sortable.serialize(this.element)) + this.observer(this.element) + } +} + +var Sortable = { + sortables: {}, + + _findRootElement: function(element) { + while (element.tagName != "BODY") { + if(element.id && Sortable.sortables[element.id]) return element; + element = element.parentNode; + } + }, + + options: function(element) { + element = Sortable._findRootElement($(element)); + if(!element) return; + return Sortable.sortables[element.id]; + }, + + destroy: function(element){ + var s = Sortable.options(element); + + if(s) { + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + + delete Sortable.sortables[s.element.id]; + } + }, + + create: function(element) { + element = $(element); + var options = Object.extend({ + element: element, + tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + tree: false, + treeTag: 'ul', + overlap: 'vertical', // one of 'vertical', 'horizontal' + constraint: 'vertical', // one of 'vertical', 'horizontal', false + containment: element, // also takes array of elements (or id's); or false + handle: false, // or a CSS class + only: false, + hoverclass: null, + ghosting: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + format: /^[^_]*_(.*)$/, + onChange: Prototype.emptyFunction, + onUpdate: Prototype.emptyFunction + }, arguments[1] || {}); + + // clear any old sortable with same element + this.destroy(element); + + // build options for the draggables + var options_for_draggable = { + revert: true, + scroll: options.scroll, + scrollSpeed: options.scrollSpeed, + scrollSensitivity: options.scrollSensitivity, + ghosting: options.ghosting, + constraint: options.constraint, + handle: options.handle }; + + if(options.starteffect) + options_for_draggable.starteffect = options.starteffect; + + if(options.reverteffect) + options_for_draggable.reverteffect = options.reverteffect; + else + if(options.ghosting) options_for_draggable.reverteffect = function(element) { + element.style.top = 0; + element.style.left = 0; + }; + + if(options.endeffect) + options_for_draggable.endeffect = options.endeffect; + + if(options.zindex) + options_for_draggable.zindex = options.zindex; + + // build options for the droppables + var options_for_droppable = { + overlap: options.overlap, + containment: options.containment, + tree: options.tree, + hoverclass: options.hoverclass, + onHover: Sortable.onHover + //greedy: !options.dropOnEmpty + } + + var options_for_tree = { + onHover: Sortable.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass + } + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // drop on empty handling + if(options.dropOnEmpty || options.tree) { + Droppables.add(element, options_for_tree); + options.droppables.push(element); + } + + (this.findElements(element, options) || []).each( function(e) { + // handles are per-draggable + var handle = options.handle ? + Element.childrenWithClassName(e, options.handle)[0] : e; + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + if(options.tree) e.treeNode = element; + options.droppables.push(e); + }); + + if(options.tree) { + (Sortable.findTreeElements(element, options) || []).each( function(e) { + Droppables.add(e, options_for_tree); + e.treeNode = element; + options.droppables.push(e); + }); + } + + // keep reference + this.sortables[element.id] = options; + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.tag); + }, + + findTreeElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.treeTag); + }, + + onHover: function(element, dropon, overlap) { + if(Element.isParent(dropon, element)) return; + + if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { + return; + } else if(overlap>0.5) { + Sortable.mark(dropon, 'before'); + if(dropon.previousSibling != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } else { + Sortable.mark(dropon, 'after'); + var nextElement = dropon.nextSibling || null; + if(nextElement != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, nextElement); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } + }, + + onEmptyHover: function(element, dropon, overlap) { + var oldParentNode = element.parentNode; + var droponOptions = Sortable.options(dropon); + + if(!Element.isParent(dropon, element)) { + var index; + + var children = Sortable.findElements(dropon, {tag: droponOptions.tag}); + var child = null; + + if(children) { + var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); + + for (index = 0; index < children.length; index += 1) { + if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { + offset -= Element.offsetSize (children[index], droponOptions.overlap); + } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { + child = index + 1 < children.length ? children[index + 1] : null; + break; + } else { + child = children[index]; + break; + } + } + } + + dropon.insertBefore(element, child); + + Sortable.options(oldParentNode).onChange(element); + droponOptions.onChange(element); + } + }, + + unmark: function() { + if(Sortable._marker) Element.hide(Sortable._marker); + }, + + mark: function(dropon, position) { + // mark on ghosting only + var sortable = Sortable.options(dropon.parentNode); + if(sortable && !sortable.ghosting) return; + + if(!Sortable._marker) { + Sortable._marker = $('dropmarker') || document.createElement('DIV'); + Element.hide(Sortable._marker); + Element.addClassName(Sortable._marker, 'dropmarker'); + Sortable._marker.style.position = 'absolute'; + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = Position.cumulativeOffset(dropon); + Sortable._marker.style.left = offsets[0] + 'px'; + Sortable._marker.style.top = offsets[1] + 'px'; + + if(position=='after') + if(sortable.overlap == 'horizontal') + Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px'; + else + Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px'; + + Element.show(Sortable._marker); + }, + + _tree: function(element, options, parent) { + var children = Sortable.findElements(element, options) || []; + + for (var i = 0; i < children.length; ++i) { + var match = children[i].id.match(options.format); + + if (!match) continue; + + var child = { + id: encodeURIComponent(match ? match[1] : null), + element: element, + parent: parent, + children: new Array, + position: parent.children.length, + container: Sortable._findChildrenElement(children[i], options.treeTag.toUpperCase()) + } + + /* Get the element containing the children and recurse over it */ + if (child.container) + this._tree(child.container, options, child) + + parent.children.push (child); + } + + return parent; + }, + + /* Finds the first element of the given tag type within a parent element. + Used for finding the first LI[ST] within a L[IST]I[TEM].*/ + _findChildrenElement: function (element, containerTag) { + if (element && element.hasChildNodes) + for (var i = 0; i < element.childNodes.length; ++i) + if (element.childNodes[i].tagName == containerTag) + return element.childNodes[i]; + + return null; + }, + + tree: function(element) { + element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + treeTag: sortableOptions.treeTag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format + }, arguments[1] || {}); + + var root = { + id: null, + parent: null, + children: new Array, + container: element, + position: 0 + } + + return Sortable._tree (element, options, root); + }, + + /* Construct a [i] index for a particular node */ + _constructIndex: function(node) { + var index = ''; + do { + if (node.id) index = '[' + node.position + ']' + index; + } while ((node = node.parent) != null); + return index; + }, + + sequence: function(element) { + element = $(element); + var options = Object.extend(this.options(element), arguments[1] || {}); + + return $(this.findElements(element, options) || []).map( function(item) { + return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; + }); + }, + + setSequence: function(element, new_sequence) { + element = $(element); + var options = Object.extend(this.options(element), arguments[2] || {}); + + var nodeMap = {}; + this.findElements(element, options).each( function(n) { + if (n.id.match(options.format)) + nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; + n.parentNode.removeChild(n); + }); + + new_sequence.each(function(ident) { + var n = nodeMap[ident]; + if (n) { + n[1].appendChild(n[0]); + delete nodeMap[ident]; + } + }); + }, + + serialize: function(element) { + element = $(element); + var options = Object.extend(Sortable.options(element), arguments[1] || {}); + var name = encodeURIComponent( + (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); + + if (options.tree) { + return Sortable.tree(element, arguments[1]).children.map( function (item) { + return [name + Sortable._constructIndex(item) + "=" + + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); + }).flatten().join('&'); + } else { + return Sortable.sequence(element, arguments[1]).map( function(item) { + return name + "[]=" + encodeURIComponent(item); + }).join('&'); + } + } +} + +/* Returns true if child is contained within element */ +Element.isParent = function(child, element) { + if (!child.parentNode || child == element) return false; + + if (child.parentNode == element) return true; + + return Element.isParent(child.parentNode, element); +} + +Element.findChildren = function(element, only, recursive, tagName) { + if(!element.hasChildNodes()) return null; + tagName = tagName.toUpperCase(); + if(only) only = [only].flatten(); + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName.toUpperCase()==tagName && + (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) + elements.push(e); + if(recursive) { + var grandchildren = Element.findChildren(e, only, recursive, tagName); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : []); +} + +Element.offsetSize = function (element, type) { + if (type == 'vertical' || type == 'height') + return element.offsetHeight; + else + return element.offsetWidth; +} \ No newline at end of file diff --git a/vendor/rails/actionpack/lib/action_view/helpers/javascripts/effects.js b/vendor/rails/actionpack/lib/action_view/helpers/javascripts/effects.js new file mode 100644 index 00000000..92740050 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/javascripts/effects.js @@ -0,0 +1,958 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// See scriptaculous.js for full license. + +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + var color = '#'; + if(this.slice(0,4) == 'rgb(') { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if(this.slice(0,1) == '#') { + if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if(this.length==7) color = this.toLowerCase(); + } + } + return(color.length==7 ? color : (arguments[0] || this)); +} + +/*--------------------------------------------------------------------------*/ + +Element.collectTextNodes = function(element) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); + }).flatten().join(''); +} + +Element.collectTextNodesIgnoreClass = function(element, className) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? + Element.collectTextNodesIgnoreClass(node, className) : '')); + }).flatten().join(''); +} + +Element.setContentZoom = function(element, percent) { + element = $(element); + Element.setStyle(element, {fontSize: (percent/100) + 'em'}); + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); +} + +Element.getOpacity = function(element){ + var opacity; + if (opacity = Element.getStyle(element, 'opacity')) + return parseFloat(opacity); + if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/)) + if(opacity[1]) return parseFloat(opacity[1]) / 100; + return 1.0; +} + +Element.setOpacity = function(element, value){ + element= $(element); + if (value == 1){ + Element.setStyle(element, { opacity: + (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? + 0.999999 : null }); + if(/MSIE/.test(navigator.userAgent)) + Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')}); + } else { + if(value < 0.00001) value = 0; + Element.setStyle(element, {opacity: value}); + if(/MSIE/.test(navigator.userAgent)) + Element.setStyle(element, + { filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') + + 'alpha(opacity='+value*100+')' }); + } +} + +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; +} + +Element.childrenWithClassName = function(element, className, findFirst) { + var classNameRegExp = new RegExp("(^|\\s)" + className + "(\\s|$)"); + var results = $A($(element).getElementsByTagName('*'))[findFirst ? 'detect' : 'select']( function(c) { + return (c.className && c.className.match(classNameRegExp)); + }); + if(!results) results = []; + return results; +} + +Element.forceRerendering = function(element) { + try { + element = $(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { } +}; + +/*--------------------------------------------------------------------------*/ + +Array.prototype.call = function() { + var args = arguments; + this.each(function(f){ f.apply(this, args) }); +} + +/*--------------------------------------------------------------------------*/ + +var Effect = { + tagifyText: function(element) { + var tagifyStyle = 'position:relative'; + if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1'; + element = $(element); + $A(element.childNodes).each( function(child) { + if(child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + Builder.node('span',{style: tagifyStyle}, + character == ' ' ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if(((typeof element == 'object') || + (typeof element == 'function')) && + (element.length)) + elements = element; + else + elements = $(element).childNodes; + + var options = Object.extend({ + speed: 0.1, + delay: 0.0 + }, arguments[2] || {}); + var masterDelay = options.delay; + + $A(elements).each( function(element, index) { + new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); + }); + }, + PAIRS: { + 'slide': ['SlideDown','SlideUp'], + 'blind': ['BlindDown','BlindUp'], + 'appear': ['Appear','Fade'] + }, + toggle: function(element, effect) { + element = $(element); + effect = (effect || 'appear').toLowerCase(); + var options = Object.extend({ + queue: { position:'end', scope:(element.id || 'global'), limit: 1 } + }, arguments[2] || {}); + Effect[element.visible() ? + Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); + } +}; + +var Effect2 = Effect; // deprecated + +/* ------------- transitions ------------- */ + +Effect.Transitions = {} + +Effect.Transitions.linear = function(pos) { + return pos; +} +Effect.Transitions.sinoidal = function(pos) { + return (-Math.cos(pos*Math.PI)/2) + 0.5; +} +Effect.Transitions.reverse = function(pos) { + return 1-pos; +} +Effect.Transitions.flicker = function(pos) { + return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; +} +Effect.Transitions.wobble = function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; +} +Effect.Transitions.pulse = function(pos) { + return (Math.floor(pos*10) % 2 == 0 ? + (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10))); +} +Effect.Transitions.none = function(pos) { + return 0; +} +Effect.Transitions.full = function(pos) { + return 1; +} + +/* ------------- core effects ------------- */ + +Effect.ScopedQueue = Class.create(); +Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), { + initialize: function() { + this.effects = []; + this.interval = null; + }, + _each: function(iterator) { + this.effects._each(iterator); + }, + add: function(effect) { + var timestamp = new Date().getTime(); + + var position = (typeof effect.options.queue == 'string') ? + effect.options.queue : effect.options.queue.position; + + switch(position) { + case 'front': + // move unstarted effects after this effect + this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + }); + break; + case 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + + if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) + this.effects.push(effect); + + if(!this.interval) + this.interval = setInterval(this.loop.bind(this), 40); + }, + remove: function(effect) { + this.effects = this.effects.reject(function(e) { return e==effect }); + if(this.effects.length == 0) { + clearInterval(this.interval); + this.interval = null; + } + }, + loop: function() { + var timePos = new Date().getTime(); + this.effects.invoke('loop', timePos); + } +}); + +Effect.Queues = { + instances: $H(), + get: function(queueName) { + if(typeof queueName != 'string') return queueName; + + if(!this.instances[queueName]) + this.instances[queueName] = new Effect.ScopedQueue(); + + return this.instances[queueName]; + } +} +Effect.Queue = Effect.Queues.get('global'); + +Effect.DefaultOptions = { + transition: Effect.Transitions.sinoidal, + duration: 1.0, // seconds + fps: 25.0, // max. 25fps due to Effect.Queue implementation + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' +} + +Effect.Base = function() {}; +Effect.Base.prototype = { + position: null, + start: function(options) { + this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {}); + this.currentFrame = 0; + this.state = 'idle'; + this.startOn = this.options.delay*1000; + this.finishOn = this.startOn + (this.options.duration*1000); + this.event('beforeStart'); + if(!this.options.sync) + Effect.Queues.get(typeof this.options.queue == 'string' ? + 'global' : this.options.queue.scope).add(this); + }, + loop: function(timePos) { + if(timePos >= this.startOn) { + if(timePos >= this.finishOn) { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + if(this.finish) this.finish(); + this.event('afterFinish'); + return; + } + var pos = (timePos - this.startOn) / (this.finishOn - this.startOn); + var frame = Math.round(pos * this.options.fps * this.options.duration); + if(frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + render: function(pos) { + if(this.state == 'idle') { + this.state = 'running'; + this.event('beforeSetup'); + if(this.setup) this.setup(); + this.event('afterSetup'); + } + if(this.state == 'running') { + if(this.options.transition) pos = this.options.transition(pos); + pos *= (this.options.to-this.options.from); + pos += this.options.from; + this.position = pos; + this.event('beforeUpdate'); + if(this.update) this.update(pos); + this.event('afterUpdate'); + } + }, + cancel: function() { + if(!this.options.sync) + Effect.Queues.get(typeof this.options.queue == 'string' ? + 'global' : this.options.queue.scope).remove(this); + this.state = 'finished'; + }, + event: function(eventName) { + if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if(this.options[eventName]) this.options[eventName](this); + }, + inspect: function() { + return '#'; + } +} + +Effect.Parallel = Class.create(); +Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { + initialize: function(effects) { + this.effects = effects || []; + this.start(arguments[1]); + }, + update: function(position) { + this.effects.invoke('render', position); + }, + finish: function(position) { + this.effects.each( function(effect) { + effect.render(1.0); + effect.cancel(); + effect.event('beforeFinish'); + if(effect.finish) effect.finish(position); + effect.event('afterFinish'); + }); + } +}); + +Effect.Opacity = Class.create(); +Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + // make this work on IE on elements without 'layout' + if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout)) + this.element.setStyle({zoom: 1}); + var options = Object.extend({ + from: this.element.getOpacity() || 0.0, + to: 1.0 + }, arguments[1] || {}); + this.start(options); + }, + update: function(position) { + this.element.setOpacity(position); + } +}); + +Effect.Move = Class.create(); +Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + var options = Object.extend({ + x: 0, + y: 0, + mode: 'relative' + }, arguments[1] || {}); + this.start(options); + }, + setup: function() { + // Bug in Opera: Opera returns the "real" position of a static element or + // relative element that does not have top/left explicitly set. + // ==> Always set top and left for position relative elements in your stylesheets + // (to 0 if you do not need them) + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if(this.options.mode == 'absolute') { + // absolute movement, so we need to calc deltaX and deltaY + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } + }, + update: function(position) { + this.element.setStyle({ + left: this.options.x * position + this.originalLeft + 'px', + top: this.options.y * position + this.originalTop + 'px' + }); + } +}); + +// for backwards compatibility +Effect.MoveBy = function(element, toTop, toLeft) { + return new Effect.Move(element, + Object.extend({ x: toLeft, y: toTop }, arguments[3] || {})); +}; + +Effect.Scale = Class.create(); +Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { + initialize: function(element, percent) { + this.element = $(element) + var options = Object.extend({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or {} with provided values + scaleFrom: 100.0, + scaleTo: percent + }, arguments[2] || {}); + this.start(options); + }, + setup: function() { + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = this.element.getStyle('position'); + + this.originalStyle = {}; + ['top','left','width','height','fontSize'].each( function(k) { + this.originalStyle[k] = this.element.style[k]; + }.bind(this)); + + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + + var fontSize = this.element.getStyle('font-size') || '100%'; + ['em','px','%'].each( function(fontSizeType) { + if(fontSize.indexOf(fontSizeType)>0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = fontSizeType; + } + }.bind(this)); + + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + + this.dims = null; + if(this.options.scaleMode=='box') + this.dims = [this.element.offsetHeight, this.element.offsetWidth]; + if(/^content/.test(this.options.scaleMode)) + this.dims = [this.element.scrollHeight, this.element.scrollWidth]; + if(!this.dims) + this.dims = [this.options.scaleMode.originalHeight, + this.options.scaleMode.originalWidth]; + }, + update: function(position) { + var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if(this.options.scaleContent && this.fontSize) + this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); + }, + setDimensions: function(height, width) { + var d = {}; + if(this.options.scaleX) d.width = width + 'px'; + if(this.options.scaleY) d.height = height + 'px'; + if(this.options.scaleFromCenter) { + var topd = (height - this.dims[0])/2; + var leftd = (width - this.dims[1])/2; + if(this.elementPositioning == 'absolute') { + if(this.options.scaleY) d.top = this.originalTop-topd + 'px'; + if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; + } else { + if(this.options.scaleY) d.top = -topd + 'px'; + if(this.options.scaleX) d.left = -leftd + 'px'; + } + } + this.element.setStyle(d); + } +}); + +Effect.Highlight = Class.create(); +Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {}); + this.start(options); + }, + setup: function() { + // Prevent executing on elements not in the layout flow + if(this.element.getStyle('display')=='none') { this.cancel(); return; } + // Disable background image during the effect + this.oldStyle = { + backgroundImage: this.element.getStyle('background-image') }; + this.element.setStyle({backgroundImage: 'none'}); + if(!this.options.endcolor) + this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); + if(!this.options.restorecolor) + this.options.restorecolor = this.element.getStyle('background-color'); + // init color calculations + this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); + this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); + }, + update: function(position) { + this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ + return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) }); + }, + finish: function() { + this.element.setStyle(Object.extend(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); + } +}); + +Effect.ScrollTo = Class.create(); +Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + this.start(arguments[1] || {}); + }, + setup: function() { + Position.prepare(); + var offsets = Position.cumulativeOffset(this.element); + if(this.options.offset) offsets[1] += this.options.offset; + var max = window.innerHeight ? + window.height - window.innerHeight : + document.body.scrollHeight - + (document.documentElement.clientHeight ? + document.documentElement.clientHeight : document.body.clientHeight); + this.scrollStart = Position.deltaY; + this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart; + }, + update: function(position) { + Position.prepare(); + window.scrollTo(Position.deltaX, + this.scrollStart + (position*this.delta)); + } +}); + +/* ------------- combination effects ------------- */ + +Effect.Fade = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + var options = Object.extend({ + from: element.getOpacity() || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { + if(effect.options.to!=0) return; + effect.element.hide(); + effect.element.setStyle({opacity: oldOpacity}); + }}, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Appear = function(element) { + element = $(element); + var options = Object.extend({ + from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), + to: 1.0, + // force Safari to render floated elements properly + afterFinishInternal: function(effect) { + effect.element.forceRerendering(); + }, + beforeSetup: function(effect) { + effect.element.setOpacity(effect.options.from); + effect.element.show(); + }}, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Puff = function(element) { + element = $(element); + var oldStyle = { opacity: element.getInlineOpacity(), position: element.getStyle('position') }; + return new Effect.Parallel( + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) { + effect.effects[0].element.setStyle({position: 'absolute'}); }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide(); + effect.effects[0].element.setStyle(oldStyle); } + }, arguments[1] || {}) + ); +} + +Effect.BlindUp = function(element) { + element = $(element); + element.makeClipping(); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + } + }, arguments[1] || {}) + ); +} + +Effect.BlindDown = function(element) { + element = $(element); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, + Object.extend({ scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makeClipping(); + effect.element.setStyle({height: '0px'}); + effect.element.show(); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + } + }, arguments[1] || {}) + ); +} + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + return new Effect.Appear(element, { + duration: 0.4, + from: 0, + transition: Effect.Transitions.flicker, + afterFinishInternal: function(effect) { + new Effect.Scale(effect.element, 1, { + duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makePositioned(); + effect.element.makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + effect.element.undoPositioned(); + effect.element.setStyle({opacity: oldOpacity}); + } + }) + } + }); +} + +Effect.DropOut = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left'), + opacity: element.getInlineOpacity() }; + return new Effect.Parallel( + [ new Effect.Move(element, {x: 0, y: 100, sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + effect.effects[0].element.makePositioned(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide(); + effect.effects[0].element.undoPositioned(); + effect.effects[0].element.setStyle(oldStyle); + } + }, arguments[1] || {})); +} + +Effect.Shake = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + effect.element.undoPositioned(); + effect.element.setStyle(oldStyle); + }}) }}) }}) }}) }}) }}); +} + +Effect.SlideDown = function(element) { + element = $(element); + element.cleanWhitespace(); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = $(element.firstChild).getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.firstChild.makePositioned(); + if(window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping(); + effect.element.setStyle({height: '0px'}); + effect.element.show(); }, + afterUpdateInternal: function(effect) { + effect.element.firstChild.setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + // IE will crash if child is undoPositioned first + if(/MSIE/.test(navigator.userAgent)){ + effect.element.undoPositioned(); + effect.element.firstChild.undoPositioned(); + }else{ + effect.element.firstChild.undoPositioned(); + effect.element.undoPositioned(); + } + effect.element.firstChild.setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || {}) + ); +} + +Effect.SlideUp = function(element) { + element = $(element); + element.cleanWhitespace(); + var oldInnerBottom = $(element.firstChild).getStyle('bottom'); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + restoreAfterFinish: true, + beforeStartInternal: function(effect) { + effect.element.makePositioned(); + effect.element.firstChild.makePositioned(); + if(window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping(); + effect.element.show(); }, + afterUpdateInternal: function(effect) { + effect.element.firstChild.setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); }, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + effect.element.firstChild.undoPositioned(); + effect.element.undoPositioned(); + effect.element.setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || {}) + ); +} + +// Bug in opera makes the TD containing this element expand for a instance after finish +Effect.Squish = function(element) { + return new Effect.Scale(element, window.opera ? 1 : 0, + { restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makeClipping(effect.element); }, + afterFinishInternal: function(effect) { + effect.element.hide(effect.element); + effect.element.undoClipping(effect.element); } + }); +} + +Effect.Grow = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.full + }, arguments[1] || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var initialMoveX, initialMoveY; + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = dims.width; + initialMoveY = moveY = 0; + moveX = -dims.width; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = dims.height; + moveY = -dims.height; + break; + case 'bottom-right': + initialMoveX = dims.width; + initialMoveY = dims.height; + moveX = -dims.width; + moveY = -dims.height; + break; + case 'center': + initialMoveX = dims.width / 2; + initialMoveY = dims.height / 2; + moveX = -dims.width / 2; + moveY = -dims.height / 2; + break; + } + + return new Effect.Move(element, { + x: initialMoveX, + y: initialMoveY, + duration: 0.01, + beforeSetup: function(effect) { + effect.element.hide(); + effect.element.makeClipping(); + effect.element.makePositioned(); + }, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), + new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), + new Effect.Scale(effect.element, 100, { + scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, + sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) + ], Object.extend({ + beforeSetup: function(effect) { + effect.effects[0].element.setStyle({height: '0px'}); + effect.effects[0].element.show(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.undoClipping(); + effect.effects[0].element.undoPositioned(); + effect.effects[0].element.setStyle(oldStyle); + } + }, options) + ) + } + }); +} + +Effect.Shrink = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.none + }, arguments[1] || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = dims.width; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = dims.height; + break; + case 'bottom-right': + moveX = dims.width; + moveY = dims.height; + break; + case 'center': + moveX = dims.width / 2; + moveY = dims.height / 2; + break; + } + + return new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), + new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + effect.effects[0].element.makePositioned(); + effect.effects[0].element.makeClipping(); }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide(); + effect.effects[0].element.undoClipping(); + effect.effects[0].element.undoPositioned(); + effect.effects[0].element.setStyle(oldStyle); } + }, options) + ); +} + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || {}; + var oldOpacity = element.getInlineOpacity(); + var transition = options.transition || Effect.Transitions.sinoidal; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) }; + reverser.bind(transition); + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 3.0, from: 0, + afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } + }, options), {transition: reverser})); +} + +Effect.Fold = function(element) { + element = $(element); + var oldStyle = { + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height }; + Element.makeClipping(element); + return new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + effect.element.setStyle(oldStyle); + } }); + }}, arguments[1] || {})); +}; + +['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom', + 'collectTextNodes','collectTextNodesIgnoreClass','childrenWithClassName'].each( + function(f) { Element.Methods[f] = Element[f]; } +); + +Element.Methods.visualEffect = function(element, effect, options) { + s = effect.gsub(/_/, '-').camelize(); + effect_class = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[effect_class](element, options); + return $(element); +}; + +Element.addMethods(); \ No newline at end of file diff --git a/vendor/rails/actionpack/lib/action_view/helpers/javascripts/prototype.js b/vendor/rails/actionpack/lib/action_view/helpers/javascripts/prototype.js new file mode 100644 index 00000000..0caf9cd7 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/javascripts/prototype.js @@ -0,0 +1,2006 @@ +/* Prototype JavaScript framework, version 1.5.0_rc0 + * (c) 2005 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://prototype.conio.net/ + * +/*--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.5.0_rc0', + ScriptFragment: '(?:)((\n|\r|.)*?)(?:<\/script>)', + + emptyFunction: function() {}, + K: function(x) {return x} +} + +var Class = { + create: function() { + return function() { + this.initialize.apply(this, arguments); + } + } +} + +var Abstract = new Object(); + +Object.extend = function(destination, source) { + for (var property in source) { + destination[property] = source[property]; + } + return destination; +} + +Object.inspect = function(object) { + try { + if (object == undefined) return 'undefined'; + if (object == null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } +} + +Function.prototype.bind = function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } +} + +Function.prototype.bindAsEventListener = function(object) { + var __method = this; + return function(event) { + return __method.call(object, event || window.event); + } +} + +Object.extend(Number.prototype, { + toColorPart: function() { + var digits = this.toString(16); + if (this < 16) return '0' + digits; + return digits; + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + } +}); + +var Try = { + these: function() { + var returnValue; + + for (var i = 0; i < arguments.length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) {} + } + + return returnValue; + } +} + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create(); +PeriodicalExecuter.prototype = { + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.callback(); + } finally { + this.currentlyExecuting = false; + } + } + } +} +Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += (replacement(match) || '').toString(); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, + + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = count === undefined ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, + + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return this; + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = truncation === undefined ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : this; + }, + + strip: function() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + }, + + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + stripScripts: function() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + }, + + extractScripts: function() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + }, + + evalScripts: function() { + return this.extractScripts().map(function(script) { return eval(script) }); + }, + + escapeHTML: function() { + var div = document.createElement('div'); + var text = document.createTextNode(this); + div.appendChild(text); + return div.innerHTML; + }, + + unescapeHTML: function() { + var div = document.createElement('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? div.childNodes[0].nodeValue : ''; + }, + + toQueryParams: function() { + var pairs = this.match(/^\??(.*)$/)[1].split('&'); + return pairs.inject({}, function(params, pairString) { + var pair = pairString.split('='); + params[pair[0]] = pair[1]; + return params; + }); + }, + + toArray: function() { + return this.split(''); + }, + + camelize: function() { + var oStringList = this.split('-'); + if (oStringList.length == 1) return oStringList[0]; + + var camelizedString = this.indexOf('-') == 0 + ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) + : oStringList[0]; + + for (var i = 1, len = oStringList.length; i < len; i++) { + var s = oStringList[i]; + camelizedString += s.charAt(0).toUpperCase() + s.substring(1); + } + + return camelizedString; + }, + + inspect: function() { + return "'" + this.replace(/\\/g, '\\\\').replace(/'/g, '\\\'') + "'"; + } +}); + +String.prototype.gsub.prepareReplacement = function(replacement) { + if (typeof replacement == 'function') return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +} + +String.prototype.parseQuery = String.prototype.toQueryParams; + +var Template = Class.create(); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; +Template.prototype = { + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + return this.template.gsub(this.pattern, function(match) { + var before = match[1]; + if (before == '\\') return match[2]; + return before + (object[match[3]] || '').toString(); + }); + } +} + +var $break = new Object(); +var $continue = new Object(); + +var Enumerable = { + each: function(iterator) { + var index = 0; + try { + this._each(function(value) { + try { + iterator(value, index++); + } catch (e) { + if (e != $continue) throw e; + } + }); + } catch (e) { + if (e != $break) throw e; + } + }, + + all: function(iterator) { + var result = true; + this.each(function(value, index) { + result = result && !!(iterator || Prototype.K)(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator) { + var result = true; + this.each(function(value, index) { + if (result = !!(iterator || Prototype.K)(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator) { + var results = []; + this.each(function(value, index) { + results.push(iterator(value, index)); + }); + return results; + }, + + detect: function (iterator) { + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator) { + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(pattern, iterator) { + var results = []; + this.each(function(value, index) { + var stringValue = value.toString(); + if (stringValue.match(pattern)) + results.push((iterator || Prototype.K)(value, index)); + }) + return results; + }, + + include: function(object) { + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inject: function(memo, iterator) { + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.collect(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator) { + var trues = [], falses = []; + this.each(function(value, index) { + ((iterator || Prototype.K)(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value, index) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator) { + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator) { + return this.collect(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.collect(Prototype.K); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (typeof args.last() == 'function') + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + }, + + inspect: function() { + return '#'; + } +} + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray +}); +var $A = Array.from = function(iterable) { + if (!iterable) return []; + if (iterable.toArray) { + return iterable.toArray(); + } else { + var results = []; + for (var i = 0; i < iterable.length; i++) + results.push(iterable[i]); + return results; + } +} + +Object.extend(Array.prototype, Enumerable); + +if (!Array.prototype._reverse) + Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0; i < this.length; i++) + iterator(this[i]); + }, + + clear: function() { + this.length = 0; + return this; + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != undefined || value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(value && value.constructor == Array ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + indexOf: function(object) { + for (var i = 0; i < this.length; i++) + if (this[i] == object) return i; + return -1; + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } +}); +var Hash = { + _each: function(iterator) { + for (var key in this) { + var value = this[key]; + if (typeof value == 'function') continue; + + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + merge: function(hash) { + return $H(hash).inject($H(this), function(mergedHash, pair) { + mergedHash[pair.key] = pair.value; + return mergedHash; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + return pair.map(encodeURIComponent).join('='); + }).join('&'); + }, + + inspect: function() { + return '#'; + } +} + +function $H(object) { + var hash = Object.extend({}, object || {}); + Object.extend(hash, Enumerable); + Object.extend(hash, Hash); + return hash; +} +ObjectRange = Class.create(); +Object.extend(ObjectRange.prototype, Enumerable); +Object.extend(ObjectRange.prototype, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + do { + iterator(value); + value = value.succ(); + } while (this.include(value)); + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +} + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new XMLHttpRequest()}, + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')} + ) || false; + }, + + activeRequestCount: 0 +} + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responderToAdd) { + if (!this.include(responderToAdd)) + this.responders.push(responderToAdd); + }, + + unregister: function(responderToRemove) { + this.responders = this.responders.without(responderToRemove); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (responder[callback] && typeof responder[callback] == 'function') { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) {} + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { + Ajax.activeRequestCount++; + }, + + onComplete: function() { + Ajax.activeRequestCount--; + } +}); + +Ajax.Base = function() {}; +Ajax.Base.prototype = { + setOptions: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + parameters: '' + } + Object.extend(this.options, options || {}); + }, + + responseIsSuccess: function() { + return this.transport.status == undefined + || this.transport.status == 0 + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + responseIsFailure: function() { + return !this.responseIsSuccess(); + } +} + +Ajax.Request = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + initialize: function(url, options) { + this.transport = Ajax.getTransport(); + this.setOptions(options); + this.request(url); + }, + + request: function(url) { + var parameters = this.options.parameters || ''; + if (parameters.length > 0) parameters += '&_='; + + try { + this.url = url; + if (this.options.method == 'get' && parameters.length > 0) + this.url += (this.url.match(/\?/) ? '&' : '?') + parameters; + + Ajax.Responders.dispatch('onCreate', this, this.transport); + + this.transport.open(this.options.method, this.url, + this.options.asynchronous); + + if (this.options.asynchronous) { + this.transport.onreadystatechange = this.onStateChange.bind(this); + setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); + } + + this.setRequestHeaders(); + + var body = this.options.postBody ? this.options.postBody : parameters; + this.transport.send(this.options.method == 'post' ? body : null); + + } catch (e) { + this.dispatchException(e); + } + }, + + setRequestHeaders: function() { + var requestHeaders = + ['X-Requested-With', 'XMLHttpRequest', + 'X-Prototype-Version', Prototype.Version, + 'Accept', 'text/javascript, text/html, application/xml, text/xml, */*']; + + if (this.options.method == 'post') { + requestHeaders.push('Content-type', this.options.contentType); + + /* Force "Connection: close" for Mozilla browsers to work around + * a bug where XMLHttpReqeuest sends an incorrect Content-length + * header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType) + requestHeaders.push('Connection', 'close'); + } + + if (this.options.requestHeaders) + requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); + + for (var i = 0; i < requestHeaders.length; i += 2) + this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState != 1) + this.respondToReadyState(this.transport.readyState); + }, + + header: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) {} + }, + + evalJSON: function() { + try { + return eval('(' + this.header('X-JSON') + ')'); + } catch (e) {} + }, + + evalResponse: function() { + try { + return eval(this.transport.responseText); + } catch (e) { + this.dispatchException(e); + } + }, + + respondToReadyState: function(readyState) { + var event = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (event == 'Complete') { + try { + (this.options['on' + this.transport.status] + || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(transport, json); + } catch (e) { + this.dispatchException(e); + } + + if ((this.header('Content-type') || '').match(/^text\/javascript/i)) + this.evalResponse(); + } + + try { + (this.options['on' + event] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + event, this, transport, json); + } catch (e) { + this.dispatchException(e); + } + + /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ + if (event == 'Complete') + this.transport.onreadystatechange = Prototype.emptyFunction; + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Updater = Class.create(); + +Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { + initialize: function(container, url, options) { + this.containers = { + success: container.success ? $(container.success) : $(container), + failure: container.failure ? $(container.failure) : + (container.success ? null : $(container)) + } + + this.transport = Ajax.getTransport(); + this.setOptions(options); + + var onComplete = this.options.onComplete || Prototype.emptyFunction; + this.options.onComplete = (function(transport, object) { + this.updateContent(); + onComplete(transport, object); + }).bind(this); + + this.request(url); + }, + + updateContent: function() { + var receiver = this.responseIsSuccess() ? + this.containers.success : this.containers.failure; + var response = this.transport.responseText; + + if (!this.options.evalScripts) + response = response.stripScripts(); + + if (receiver) { + if (this.options.insertion) { + new this.options.insertion(receiver, response); + } else { + Element.update(receiver, response); + } + } + + if (this.responseIsSuccess()) { + if (this.onComplete) + setTimeout(this.onComplete.bind(this), 10); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(); +Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { + initialize: function(container, url, options) { + this.setOptions(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = {}; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(request) { + if (this.options.decay) { + this.decay = (request.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = request.responseText; + } + this.timer = setTimeout(this.onTimerEvent.bind(this), + this.decay * this.frequency * 1000); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +function $() { + var results = [], element; + for (var i = 0; i < arguments.length; i++) { + element = arguments[i]; + if (typeof element == 'string') + element = document.getElementById(element); + results.push(Element.extend(element)); + } + return results.length < 2 ? results[0] : results; +} + +document.getElementsByClassName = function(className, parentElement) { + var children = ($(parentElement) || document.body).getElementsByTagName('*'); + return $A(children).inject([], function(elements, child) { + if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) + elements.push(Element.extend(child)); + return elements; + }); +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Element) + var Element = new Object(); + +Element.extend = function(element) { + if (!element) return; + if (_nativeExtensions) return element; + + if (!element._extended && element.tagName && element != window) { + var methods = Element.Methods, cache = Element.extend.cache; + for (property in methods) { + var value = methods[property]; + if (typeof value == 'function') + element[property] = cache.findOrStore(value); + } + } + + element._extended = true; + return element; +} + +Element.extend.cache = { + findOrStore: function(value) { + return this[value] = this[value] || function() { + return value.apply(null, [this].concat($A(arguments))); + } + } +} + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + Element[Element.visible(element) ? 'hide' : 'show'](element); + } + }, + + hide: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = 'none'; + } + }, + + show: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = ''; + } + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + }, + + update: function(element, html) { + $(element).innerHTML = html.stripScripts(); + setTimeout(function() {html.evalScripts()}, 10); + }, + + replace: function(element, html) { + element = $(element); + if (element.outerHTML) { + element.outerHTML = html.stripScripts(); + } else { + var range = element.ownerDocument.createRange(); + range.selectNodeContents(element); + element.parentNode.replaceChild( + range.createContextualFragment(html.stripScripts()), element); + } + setTimeout(function() {html.evalScripts()}, 10); + }, + + getHeight: function(element) { + element = $(element); + return element.offsetHeight; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).include(className); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).add(className); + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).remove(className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + for (var i = 0; i < element.childNodes.length; i++) { + var node = element.childNodes[i]; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + Element.remove(node); + } + }, + + empty: function(element) { + return $(element).innerHTML.match(/^\s*$/); + }, + + childOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + while (element = element.parentNode) + if (element == ancestor) return true; + return false; + }, + + scrollTo: function(element) { + element = $(element); + var x = element.x ? element.x : element.offsetLeft, + y = element.y ? element.y : element.offsetTop; + window.scrollTo(x, y); + }, + + getStyle: function(element, style) { + element = $(element); + var value = element.style[style.camelize()]; + if (!value) { + if (document.defaultView && document.defaultView.getComputedStyle) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css.getPropertyValue(style) : null; + } else if (element.currentStyle) { + value = element.currentStyle[style.camelize()]; + } + } + + if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) + if (Element.getStyle(element, 'position') == 'static') value = 'auto'; + + return value == 'auto' ? null : value; + }, + + setStyle: function(element, style) { + element = $(element); + for (var name in style) + element.style[name.camelize()] = style[name]; + }, + + getDimensions: function(element) { + element = $(element); + if (Element.getStyle(element, 'display') != 'none') + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = ''; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = 'none'; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (window.opera) { + element.style.top = 0; + element.style.left = 0; + } + } + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return; + element._overflow = element.style.overflow; + if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') + element.style.overflow = 'hidden'; + }, + + undoClipping: function(element) { + element = $(element); + if (element._overflow) return; + element.style.overflow = element._overflow; + element._overflow = undefined; + } +} + +Object.extend(Element, Element.Methods); + +var _nativeExtensions = false; + +if(!HTMLElement && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + var HTMLElement = {} + HTMLElement.prototype = document.createElement('div').__proto__; +} + +Element.addMethods = function(methods) { + Object.extend(Element.Methods, methods || {}); + + if(typeof HTMLElement != 'undefined') { + var methods = Element.Methods, cache = Element.extend.cache; + for (property in methods) { + var value = methods[property]; + if (typeof value == 'function') + HTMLElement.prototype[property] = cache.findOrStore(value); + } + _nativeExtensions = true; + } +} + +Element.addMethods(); + +var Toggle = new Object(); +Toggle.display = Element.toggle; + +/*--------------------------------------------------------------------------*/ + +Abstract.Insertion = function(adjacency) { + this.adjacency = adjacency; +} + +Abstract.Insertion.prototype = { + initialize: function(element, content) { + this.element = $(element); + this.content = content.stripScripts(); + + if (this.adjacency && this.element.insertAdjacentHTML) { + try { + this.element.insertAdjacentHTML(this.adjacency, this.content); + } catch (e) { + var tagName = this.element.tagName.toLowerCase(); + if (tagName == 'tbody' || tagName == 'tr') { + this.insertContent(this.contentFromAnonymousTable()); + } else { + throw e; + } + } + } else { + this.range = this.element.ownerDocument.createRange(); + if (this.initializeRange) this.initializeRange(); + this.insertContent([this.range.createContextualFragment(this.content)]); + } + + setTimeout(function() {content.evalScripts()}, 10); + }, + + contentFromAnonymousTable: function() { + var div = document.createElement('div'); + div.innerHTML = '' + this.content + '
      '; + return $A(div.childNodes[0].childNodes[0].childNodes); + } +} + +var Insertion = new Object(); + +Insertion.Before = Class.create(); +Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { + initializeRange: function() { + this.range.setStartBefore(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, this.element); + }).bind(this)); + } +}); + +Insertion.Top = Class.create(); +Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(true); + }, + + insertContent: function(fragments) { + fragments.reverse(false).each((function(fragment) { + this.element.insertBefore(fragment, this.element.firstChild); + }).bind(this)); + } +}); + +Insertion.Bottom = Class.create(); +Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.appendChild(fragment); + }).bind(this)); + } +}); + +Insertion.After = Class.create(); +Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { + initializeRange: function() { + this.range.setStartAfter(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, + this.element.nextSibling); + }).bind(this)); + } +}); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, + + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, + + set: function(className) { + this.element.className = className; + }, + + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set(this.toArray().concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set(this.select(function(className) { + return className != classNameToRemove; + }).join(' ')); + }, + + toString: function() { + return this.toArray().join(' '); + } +} + +Object.extend(Element.ClassNames.prototype, Enumerable); +var Selector = Class.create(); +Selector.prototype = { + initialize: function(expression) { + this.params = {classNames: []}; + this.expression = expression.toString().strip(); + this.parseExpression(); + this.compileMatcher(); + }, + + parseExpression: function() { + function abort(message) { throw 'Parse error in selector: ' + message; } + + if (this.expression == '') abort('empty expression'); + + var params = this.params, expr = this.expression, match, modifier, clause, rest; + while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) { + params.attributes = params.attributes || []; + params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''}); + expr = match[1]; + } + + if (expr == '*') return this.params.wildcard = true; + + while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) { + modifier = match[1], clause = match[2], rest = match[3]; + switch (modifier) { + case '#': params.id = clause; break; + case '.': params.classNames.push(clause); break; + case '': + case undefined: params.tagName = clause.toUpperCase(); break; + default: abort(expr.inspect()); + } + expr = rest; + } + + if (expr.length > 0) abort(expr.inspect()); + }, + + buildMatchExpression: function() { + var params = this.params, conditions = [], clause; + + if (params.wildcard) + conditions.push('true'); + if (clause = params.id) + conditions.push('element.id == ' + clause.inspect()); + if (clause = params.tagName) + conditions.push('element.tagName.toUpperCase() == ' + clause.inspect()); + if ((clause = params.classNames).length > 0) + for (var i = 0; i < clause.length; i++) + conditions.push('Element.hasClassName(element, ' + clause[i].inspect() + ')'); + if (clause = params.attributes) { + clause.each(function(attribute) { + var value = 'element.getAttribute(' + attribute.name.inspect() + ')'; + var splitValueBy = function(delimiter) { + return value + ' && ' + value + '.split(' + delimiter.inspect() + ')'; + } + + switch (attribute.operator) { + case '=': conditions.push(value + ' == ' + attribute.value.inspect()); break; + case '~=': conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break; + case '|=': conditions.push( + splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect() + ); break; + case '!=': conditions.push(value + ' != ' + attribute.value.inspect()); break; + case '': + case undefined: conditions.push(value + ' != null'); break; + default: throw 'Unknown operator ' + attribute.operator + ' in selector'; + } + }); + } + + return conditions.join(' && '); + }, + + compileMatcher: function() { + this.match = new Function('element', 'if (!element.tagName) return false; \ + return ' + this.buildMatchExpression()); + }, + + findElements: function(scope) { + var element; + + if (element = $(this.params.id)) + if (this.match(element)) + if (!scope || Element.childOf(element, scope)) + return [element]; + + scope = (scope || document).getElementsByTagName(this.params.tagName || '*'); + + var results = []; + for (var i = 0; i < scope.length; i++) + if (this.match(element = scope[i])) + results.push(Element.extend(element)); + + return results; + }, + + toString: function() { + return this.expression; + } +} + +function $$() { + return $A(arguments).map(function(expression) { + return expression.strip().split(/\s+/).inject([null], function(results, expr) { + var selector = new Selector(expr); + return results.map(selector.findElements.bind(selector)).flatten(); + }); + }).flatten(); +} +var Field = { + clear: function() { + for (var i = 0; i < arguments.length; i++) + $(arguments[i]).value = ''; + }, + + focus: function(element) { + $(element).focus(); + }, + + present: function() { + for (var i = 0; i < arguments.length; i++) + if ($(arguments[i]).value == '') return false; + return true; + }, + + select: function(element) { + $(element).select(); + }, + + activate: function(element) { + element = $(element); + element.focus(); + if (element.select) + element.select(); + } +} + +/*--------------------------------------------------------------------------*/ + +var Form = { + serialize: function(form) { + var elements = Form.getElements($(form)); + var queryComponents = new Array(); + + for (var i = 0; i < elements.length; i++) { + var queryComponent = Form.Element.serialize(elements[i]); + if (queryComponent) + queryComponents.push(queryComponent); + } + + return queryComponents.join('&'); + }, + + getElements: function(form) { + form = $(form); + var elements = new Array(); + + for (var tagName in Form.Element.Serializers) { + var tagElements = form.getElementsByTagName(tagName); + for (var j = 0; j < tagElements.length; j++) + elements.push(tagElements[j]); + } + return elements; + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) + return inputs; + + var matchingInputs = new Array(); + for (var i = 0; i < inputs.length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || + (name && input.name != name)) + continue; + matchingInputs.push(input); + } + + return matchingInputs; + }, + + disable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.blur(); + element.disabled = 'true'; + } + }, + + enable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.disabled = ''; + } + }, + + findFirstElement: function(form) { + return Form.getElements(form).find(function(element) { + return element.type != 'hidden' && !element.disabled && + ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + Field.activate(Form.findFirstElement(form)); + }, + + reset: function(form) { + $(form).reset(); + } +} + +Form.Element = { + serialize: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) { + var key = encodeURIComponent(parameter[0]); + if (key.length == 0) return; + + if (parameter[1].constructor != Array) + parameter[1] = [parameter[1]]; + + return parameter[1].map(function(value) { + return key + '=' + encodeURIComponent(value); + }).join('&'); + } + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) + return parameter[1]; + } +} + +Form.Element.Serializers = { + input: function(element) { + switch (element.type.toLowerCase()) { + case 'submit': + case 'hidden': + case 'password': + case 'text': + return Form.Element.Serializers.textarea(element); + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element); + } + return false; + }, + + inputSelector: function(element) { + if (element.checked) + return [element.name, element.value]; + }, + + textarea: function(element) { + return [element.name, element.value]; + }, + + select: function(element) { + return Form.Element.Serializers[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + + selectOne: function(element) { + var value = '', opt, index = element.selectedIndex; + if (index >= 0) { + opt = element.options[index]; + value = opt.value || opt.text; + } + return [element.name, value]; + }, + + selectMany: function(element) { + var value = []; + for (var i = 0; i < element.length; i++) { + var opt = element.options[i]; + if (opt.selected) + value.push(opt.value || opt.text); + } + return [element.name, value]; + } +} + +/*--------------------------------------------------------------------------*/ + +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = function() {} +Abstract.TimedObserver.prototype = { + initialize: function(element, frequency, callback) { + this.frequency = frequency; + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + } +} + +Form.Element.Observer = Class.create(); +Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(); +Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = function() {} +Abstract.EventObserver.prototype = { + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + var elements = Form.getElements(this.element); + for (var i = 0; i < elements.length; i++) + this.registerCallback(elements[i]); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + case 'password': + case 'text': + case 'textarea': + case 'select-one': + case 'select-multiple': + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +} + +Form.Element.EventObserver = Class.create(); +Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(); +Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) { + var Event = new Object(); +} + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + + element: function(event) { + return event.target || event.srcElement; + }, + + isLeftClick: function(event) { + return (((event.which) && (event.which == 1)) || + ((event.button) && (event.button == 1))); + }, + + pointerX: function(event) { + return event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)); + }, + + pointerY: function(event) { + return event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)); + }, + + stop: function(event) { + if (event.preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } else { + event.returnValue = false; + event.cancelBubble = true; + } + }, + + // find the first node with the given tagName, starting from the + // node the event was triggered on; traverses the DOM upwards + findElement: function(event, tagName) { + var element = Event.element(event); + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))) + element = element.parentNode; + return element; + }, + + observers: false, + + _observeAndCache: function(element, name, observer, useCapture) { + if (!this.observers) this.observers = []; + if (element.addEventListener) { + this.observers.push([element, name, observer, useCapture]); + element.addEventListener(name, observer, useCapture); + } else if (element.attachEvent) { + this.observers.push([element, name, observer, useCapture]); + element.attachEvent('on' + name, observer); + } + }, + + unloadCache: function() { + if (!Event.observers) return; + for (var i = 0; i < Event.observers.length; i++) { + Event.stopObserving.apply(this, Event.observers[i]); + Event.observers[i][0] = null; + } + Event.observers = false; + }, + + observe: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.attachEvent)) + name = 'keydown'; + + this._observeAndCache(element, name, observer, useCapture); + }, + + stopObserving: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.detachEvent)) + name = 'keydown'; + + if (element.removeEventListener) { + element.removeEventListener(name, observer, useCapture); + } else if (element.detachEvent) { + element.detachEvent('on' + name, observer); + } + } +}); + +/* prevent memory leaks in IE */ +if (navigator.appVersion.match(/\bMSIE\b/)) + Event.observe(window, 'unload', Event.unloadCache, false); +var Position = { + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + // must be called before calling withinIncludingScrolloffset, every time the + // page is scrolled + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + realOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return [valueL, valueT]; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return [valueL, valueT]; + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return [valueL, valueT]; + }, + + offsetParent: function(element) { + if (element.offsetParent) return element.offsetParent; + if (element == document.body) return element; + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return element; + + return document.body; + }, + + // caches x/y coordinate pair to use with overlap + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = this.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = this.realOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = this.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + // within must be called directly before + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + clone: function(source, target) { + source = $(source); + target = $(target); + target.style.position = 'absolute'; + var offsets = this.cumulativeOffset(source); + target.style.top = offsets[1] + 'px'; + target.style.left = offsets[0] + 'px'; + target.style.width = source.offsetWidth + 'px'; + target.style.height = source.offsetHeight + 'px'; + }, + + page: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent==document.body) + if (Element.getStyle(element,'position')=='absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } while (element = element.parentNode); + + return [valueL, valueT]; + }, + + clone: function(source, target) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || {}) + + // find page position of source + source = $(source); + var p = Position.page(source); + + // find coordinate system to use + target = $(target); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(target,'position') == 'absolute') { + parent = Position.offsetParent(target); + delta = Position.page(parent); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if(options.setWidth) target.style.width = source.offsetWidth + 'px'; + if(options.setHeight) target.style.height = source.offsetHeight + 'px'; + }, + + absolutize: function(element) { + element = $(element); + if (element.style.position == 'absolute') return; + Position.prepare(); + + var offsets = Position.positionedOffset(element); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px';; + element.style.left = left + 'px';; + element.style.width = width + 'px';; + element.style.height = height + 'px';; + }, + + relativize: function(element) { + element = $(element); + if (element.style.position == 'relative') return; + Position.prepare(); + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + } +} + +// Safari returns margins on body which is incorrect if the child is absolutely +// positioned. For performance reasons, redefine Position.cumulativeOffset for +// KHTML/WebKit only. +if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + Position.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return [valueL, valueT]; + } +} \ No newline at end of file diff --git a/vendor/rails/actionpack/lib/action_view/helpers/number_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/number_helper.rb new file mode 100644 index 00000000..9098dd8c --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/number_helper.rb @@ -0,0 +1,109 @@ +module ActionView + module Helpers + # Provides methods for converting a number into a formatted string that currently represents + # one of the following forms: phone number, percentage, money, or precision level. + module NumberHelper + + # Formats a +number+ into a US phone number string. The +options+ can be a hash used to customize the format of the output. + # The area code can be surrounded by parentheses by setting +:area_code+ to true; default is false + # The delimiter can be set using +:delimiter+; default is "-" + # Examples: + # number_to_phone(1235551234) => 123-555-1234 + # number_to_phone(1235551234, {:area_code => true}) => (123) 555-1234 + # number_to_phone(1235551234, {:delimiter => " "}) => 123 555 1234 + # number_to_phone(1235551234, {:area_code => true, :extension => 555}) => (123) 555-1234 x 555 + def number_to_phone(number, options = {}) + options = options.stringify_keys + area_code = options.delete("area_code") { false } + delimiter = options.delete("delimiter") { "-" } + extension = options.delete("extension") { "" } + begin + str = area_code == true ? number.to_s.gsub(/([0-9]{3})([0-9]{3})([0-9]{4})/,"(\\1) \\2#{delimiter}\\3") : number.to_s.gsub(/([0-9]{3})([0-9]{3})([0-9]{4})/,"\\1#{delimiter}\\2#{delimiter}\\3") + extension.to_s.strip.empty? ? str : "#{str} x #{extension.to_s.strip}" + rescue + number + end + end + + # Formats a +number+ into a currency string. The +options+ hash can be used to customize the format of the output. + # The +number+ can contain a level of precision using the +precision+ key; default is 2 + # The currency type can be set using the +unit+ key; default is "$" + # The unit separator can be set using the +separator+ key; default is "." + # The delimiter can be set using the +delimiter+ key; default is "," + # Examples: + # number_to_currency(1234567890.50) => $1,234,567,890.50 + # number_to_currency(1234567890.506) => $1,234,567,890.51 + # number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""}) => £1234567890,50 + def number_to_currency(number, options = {}) + options = options.stringify_keys + precision, unit, separator, delimiter = options.delete("precision") { 2 }, options.delete("unit") { "$" }, options.delete("separator") { "." }, options.delete("delimiter") { "," } + separator = "" unless precision > 0 + begin + parts = number_with_precision(number, precision).split('.') + unit + number_with_delimiter(parts[0], delimiter) + separator + parts[1].to_s + rescue + number + end + end + + # Formats a +number+ as into a percentage string. The +options+ hash can be used to customize the format of the output. + # The +number+ can contain a level of precision using the +precision+ key; default is 3 + # The unit separator can be set using the +separator+ key; default is "." + # Examples: + # number_to_percentage(100) => 100.000% + # number_to_percentage(100, {:precision => 0}) => 100% + # number_to_percentage(302.0574, {:precision => 2}) => 302.06% + def number_to_percentage(number, options = {}) + options = options.stringify_keys + precision, separator = options.delete("precision") { 3 }, options.delete("separator") { "." } + begin + number = number_with_precision(number, precision) + parts = number.split('.') + if parts.at(1).nil? + parts[0] + "%" + else + parts[0] + separator + parts[1].to_s + "%" + end + rescue + number + end + end + + # Formats a +number+ with a +delimiter+. + # Example: + # number_with_delimiter(12345678) => 12,345,678 + def number_with_delimiter(number, delimiter=",") + number.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}") + end + + # Returns a formatted-for-humans file size. + # + # Examples: + # human_size(123) => 123 Bytes + # human_size(1234) => 1.2 KB + # human_size(12345) => 12.1 KB + # human_size(1234567) => 1.2 MB + # human_size(1234567890) => 1.1 GB + def number_to_human_size(size) + case + when size < 1.kilobyte: '%d Bytes' % size + when size < 1.megabyte: '%.1f KB' % (size / 1.0.kilobyte) + when size < 1.gigabyte: '%.1f MB' % (size / 1.0.megabyte) + when size < 1.terabyte: '%.1f GB' % (size / 1.0.gigabyte) + else '%.1f TB' % (size / 1.0.terabyte) + end.sub('.0', '') + rescue + nil + end + + alias_method :human_size, :number_to_human_size # deprecated alias + + # Formats a +number+ with a level of +precision+. + # Example: + # number_with_precision(111.2345) => 111.235 + def number_with_precision(number, precision=3) + sprintf("%01.#{precision}f", number) + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/pagination_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/pagination_helper.rb new file mode 100644 index 00000000..6123b738 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/pagination_helper.rb @@ -0,0 +1,86 @@ +module ActionView + module Helpers + # Provides methods for linking to ActionController::Pagination objects. + # + # You can also build your links manually, like in this example: + # + # <%= link_to "Previous page", { :page => paginator.current.previous } if paginator.current.previous %> + # + # <%= link_to "Next page", { :page => paginator.current.next } if paginator.current.next %> + module PaginationHelper + unless const_defined?(:DEFAULT_OPTIONS) + DEFAULT_OPTIONS = { + :name => :page, + :window_size => 2, + :always_show_anchors => true, + :link_to_current_page => false, + :params => {} + } + end + + # Creates a basic HTML link bar for the given +paginator+. + # +html_options+ are passed to +link_to+. + # + # +options+ are: + # :name:: the routing name for this paginator + # (defaults to +page+) + # :window_size:: the number of pages to show around + # the current page (defaults to +2+) + # :always_show_anchors:: whether or not the first and last + # pages should always be shown + # (defaults to +true+) + # :link_to_current_page:: whether or not the current page + # should be linked to (defaults to + # +false+) + # :params:: any additional routing parameters + # for page URLs + def pagination_links(paginator, options={}, html_options={}) + name = options[:name] || DEFAULT_OPTIONS[:name] + params = (options[:params] || DEFAULT_OPTIONS[:params]).clone + + pagination_links_each(paginator, options) do |n| + params[name] = n + link_to(n.to_s, params, html_options) + end + end + + # Iterate through the pages of a given +paginator+, invoking a + # block for each page number that needs to be rendered as a link. + def pagination_links_each(paginator, options) + options = DEFAULT_OPTIONS.merge(options) + link_to_current_page = options[:link_to_current_page] + always_show_anchors = options[:always_show_anchors] + + current_page = paginator.current_page + window_pages = current_page.window(options[:window_size]).pages + return if window_pages.length <= 1 unless link_to_current_page + + first, last = paginator.first, paginator.last + + html = '' + if always_show_anchors and not (wp_first = window_pages[0]).first? + html << yield(first.number) + html << ' ... ' if wp_first.number - first.number > 1 + html << ' ' + end + + window_pages.each do |page| + if current_page == page && !link_to_current_page + html << page.number.to_s + else + html << yield(page.number) + end + html << ' ' + end + + if always_show_anchors and not (wp_last = window_pages[-1]).last? + html << ' ... ' if last.number - wp_last.number > 1 + html << yield(last.number) + end + + html + end + + end # PaginationHelper + end # Helpers +end # ActionView diff --git a/vendor/rails/actionpack/lib/action_view/helpers/prototype_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/prototype_helper.rb new file mode 100644 index 00000000..3c4c0ba9 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -0,0 +1,901 @@ +require File.dirname(__FILE__) + '/javascript_helper' +require 'set' + +module ActionView + module Helpers + # Provides a set of helpers for calling Prototype JavaScript functions, + # including functionality to call remote methods using + # Ajax[http://www.adaptivepath.com/publications/essays/archives/000385.php]. + # This means that you can call actions in your controllers without + # reloading the page, but still update certain parts of it using + # injections into the DOM. The common use case is having a form that adds + # a new element to a list without reloading the page. + # + # To be able to use these helpers, you must include the Prototype + # JavaScript framework in your pages. See the documentation for + # ActionView::Helpers::JavaScriptHelper for more information on including + # the necessary JavaScript. + # + # See link_to_remote for documentation of options common to all Ajax + # helpers. + # + # See also ActionView::Helpers::ScriptaculousHelper for helpers which work + # with the Scriptaculous controls and visual effects library. + # + # See JavaScriptGenerator for information on updating multiple elements + # on the page in an Ajax response. + module PrototypeHelper + unless const_defined? :CALLBACKS + CALLBACKS = Set.new([ :uninitialized, :loading, :loaded, + :interactive, :complete, :failure, :success ] + + (100..599).to_a) + AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url, + :asynchronous, :method, :insertion, :position, + :form, :with, :update, :script ]).merge(CALLBACKS) + end + + # Returns a link to a remote action defined by options[:url] + # (using the url_for format) that's called in the background using + # XMLHttpRequest. The result of that request can then be inserted into a + # DOM object whose id can be specified with options[:update]. + # Usually, the result would be a partial prepared by the controller with + # either render_partial or render_partial_collection. + # + # Examples: + # link_to_remote "Delete this post", :update => "posts", + # :url => { :action => "destroy", :id => post.id } + # link_to_remote(image_tag("refresh"), :update => "emails", + # :url => { :action => "list_emails" }) + # + # You can also specify a hash for options[:update] to allow for + # easy redirection of output to an other DOM element if a server-side + # error occurs: + # + # Example: + # link_to_remote "Delete this post", + # :url => { :action => "destroy", :id => post.id }, + # :update => { :success => "posts", :failure => "error" } + # + # Optionally, you can use the options[:position] parameter to + # influence how the target DOM element is updated. It must be one of + # :before, :top, :bottom, or :after. + # + # By default, these remote requests are processed asynchronous during + # which various JavaScript callbacks can be triggered (for progress + # indicators and the likes). All callbacks get access to the + # request object, which holds the underlying XMLHttpRequest. + # + # To access the server response, use request.responseText, to + # find out the HTTP status, use request.status. + # + # Example: + # link_to_remote word, + # :url => { :action => "undo", :n => word_counter }, + # :complete => "undoRequestCompleted(request)" + # + # The callbacks that may be specified are (in order): + # + # :loading:: Called when the remote document is being + # loaded with data by the browser. + # :loaded:: Called when the browser has finished loading + # the remote document. + # :interactive:: Called when the user can interact with the + # remote document, even though it has not + # finished loading. + # :success:: Called when the XMLHttpRequest is completed, + # and the HTTP status code is in the 2XX range. + # :failure:: Called when the XMLHttpRequest is completed, + # and the HTTP status code is not in the 2XX + # range. + # :complete:: Called when the XMLHttpRequest is complete + # (fires after success/failure if they are + # present). + # + # You can further refine :success and :failure by + # adding additional callbacks for specific status codes. + # + # Example: + # link_to_remote word, + # :url => { :action => "action" }, + # 404 => "alert('Not found...? Wrong URL...?')", + # :failure => "alert('HTTP Error ' + request.status + '!')" + # + # A status code callback overrides the success/failure handlers if + # present. + # + # If you for some reason or another need synchronous processing (that'll + # block the browser while the request is happening), you can specify + # options[:type] = :synchronous. + # + # You can customize further browser side call logic by passing in + # JavaScript code snippets via some optional parameters. In their order + # of use these are: + # + # :confirm:: Adds confirmation dialog. + # :condition:: Perform remote request conditionally + # by this expression. Use this to + # describe browser-side conditions when + # request should not be initiated. + # :before:: Called before request is initiated. + # :after:: Called immediately after request was + # initiated and before :loading. + # :submit:: Specifies the DOM element ID that's used + # as the parent of the form elements. By + # default this is the current form, but + # it could just as well be the ID of a + # table row or any other DOM element. + def link_to_remote(name, options = {}, html_options = {}) + link_to_function(name, remote_function(options), html_options) + end + + # Periodically calls the specified url (options[:url]) every + # options[:frequency] seconds (default is 10). Usually used to + # update a specified div (options[:update]) with the results + # of the remote call. The options for specifying the target with :url + # and defining callbacks is the same as link_to_remote. + def periodically_call_remote(options = {}) + frequency = options[:frequency] || 10 # every ten seconds by default + code = "new PeriodicalExecuter(function() {#{remote_function(options)}}, #{frequency})" + javascript_tag(code) + end + + # Returns a form tag that will submit using XMLHttpRequest in the + # background instead of the regular reloading POST arrangement. Even + # though it's using JavaScript to serialize the form elements, the form + # submission will work just like a regular submission as viewed by the + # receiving side (all elements available in params). The options for + # specifying the target with :url and defining callbacks is the same as + # link_to_remote. + # + # A "fall-through" target for browsers that doesn't do JavaScript can be + # specified with the :action/:method options on :html. + # + # Example: + # form_remote_tag :html => { :action => + # url_for(:controller => "some", :action => "place") } + # + # The Hash passed to the :html key is equivalent to the options (2nd) + # argument in the FormTagHelper.form_tag method. + # + # By default the fall-through action is the same as the one specified in + # the :url (and the default method is :post). + def form_remote_tag(options = {}) + options[:form] = true + + options[:html] ||= {} + options[:html][:onsubmit] = "#{remote_function(options)}; return false;" + options[:html][:action] = options[:html][:action] || url_for(options[:url]) + options[:html][:method] = options[:html][:method] || "post" + + tag("form", options[:html], true) + end + + # Works like form_remote_tag, but uses form_for semantics. + def remote_form_for(object_name, *args, &proc) + options = args.last.is_a?(Hash) ? args.pop : {} + concat(form_remote_tag(options), proc.binding) + fields_for(object_name, *(args << options), &proc) + concat('', proc.binding) + end + alias_method :form_remote_for, :remote_form_for + + # Returns a button input tag that will submit form using XMLHttpRequest + # in the background instead of regular reloading POST arrangement. + # options argument is the same as in form_remote_tag. + def submit_to_remote(name, value, options = {}) + options[:with] ||= 'Form.serialize(this.form)' + + options[:html] ||= {} + options[:html][:type] = 'button' + options[:html][:onclick] = "#{remote_function(options)}; return false;" + options[:html][:name] = name + options[:html][:value] = value + + tag("input", options[:html], false) + end + + # Returns a JavaScript function (or expression) that'll update a DOM + # element according to the options passed. + # + # * :content: The content to use for updating. Can be left out + # if using block, see example. + # * :action: Valid options are :update (assumed by default), + # :empty, :remove + # * :position If the :action is :update, you can optionally + # specify one of the following positions: :before, :top, :bottom, + # :after. + # + # Examples: + # <%= javascript_tag(update_element_function("products", + # :position => :bottom, :content => "

      New product!

      ")) %> + # + # <% replacement_function = update_element_function("products") do %> + #

      Product 1

      + #

      Product 2

      + # <% end %> + # <%= javascript_tag(replacement_function) %> + # + # This method can also be used in combination with remote method call + # where the result is evaluated afterwards to cause multiple updates on + # a page. Example: + # + # # Calling view + # <%= form_remote_tag :url => { :action => "buy" }, + # :complete => evaluate_remote_response %> + # all the inputs here... + # + # # Controller action + # def buy + # @product = Product.find(1) + # end + # + # # Returning view + # <%= update_element_function( + # "cart", :action => :update, :position => :bottom, + # :content => "

      New Product: #{@product.name}

      ")) %> + # <% update_element_function("status", :binding => binding) do %> + # You've bought a new product! + # <% end %> + # + # Notice how the second call doesn't need to be in an ERb output block + # since it uses a block and passes in the binding to render directly. + # This trick will however only work in ERb (not Builder or other + # template forms). + # + # See also JavaScriptGenerator and update_page. + def update_element_function(element_id, options = {}, &block) + content = escape_javascript(options[:content] || '') + content = escape_javascript(capture(&block)) if block + + javascript_function = case (options[:action] || :update) + when :update + if options[:position] + "new Insertion.#{options[:position].to_s.camelize}('#{element_id}','#{content}')" + else + "$('#{element_id}').innerHTML = '#{content}'" + end + + when :empty + "$('#{element_id}').innerHTML = ''" + + when :remove + "Element.remove('#{element_id}')" + + else + raise ArgumentError, "Invalid action, choose one of :update, :remove, :empty" + end + + javascript_function << ";\n" + options[:binding] ? concat(javascript_function, options[:binding]) : javascript_function + end + + # Returns 'eval(request.responseText)' which is the JavaScript function + # that form_remote_tag can call in :complete to evaluate a multiple + # update return document using update_element_function calls. + def evaluate_remote_response + "eval(request.responseText)" + end + + # Returns the JavaScript needed for a remote function. + # Takes the same arguments as link_to_remote. + # + # Example: + # + def remote_function(options) + javascript_options = options_for_ajax(options) + + update = '' + if options[:update] and options[:update].is_a?Hash + update = [] + update << "success:'#{options[:update][:success]}'" if options[:update][:success] + update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure] + update = '{' + update.join(',') + '}' + elsif options[:update] + update << "'#{options[:update]}'" + end + + function = update.empty? ? + "new Ajax.Request(" : + "new Ajax.Updater(#{update}, " + + url_options = options[:url] + url_options = url_options.merge(:escape => false) if url_options.is_a? Hash + function << "'#{url_for(url_options)}'" + function << ", #{javascript_options})" + + function = "#{options[:before]}; #{function}" if options[:before] + function = "#{function}; #{options[:after]}" if options[:after] + function = "if (#{options[:condition]}) { #{function}; }" if options[:condition] + function = "if (confirm('#{escape_javascript(options[:confirm])}')) { #{function}; }" if options[:confirm] + + return function + end + + # Observes the field with the DOM ID specified by +field_id+ and makes + # an Ajax call when its contents have changed. + # + # Required +options+ are either of: + # :url:: +url_for+-style options for the action to call + # when the field has changed. + # :function:: Instead of making a remote call to a URL, you + # can specify a function to be called instead. + # + # Additional options are: + # :frequency:: The frequency (in seconds) at which changes to + # this field will be detected. Not setting this + # option at all or to a value equal to or less than + # zero will use event based observation instead of + # time based observation. + # :update:: Specifies the DOM ID of the element whose + # innerHTML should be updated with the + # XMLHttpRequest response text. + # :with:: A JavaScript expression specifying the + # parameters for the XMLHttpRequest. This defaults + # to 'value', which in the evaluated context + # refers to the new field value. If you specify a + # string without a "=", it'll be extended to mean + # the form key that the value should be assigned to. + # So :with => "term" gives "'term'=value". If a "=" is + # present, no extension will happen. + # :on:: Specifies which event handler to observe. By default, + # it's set to "changed" for text fields and areas and + # "click" for radio buttons and checkboxes. With this, + # you can specify it instead to be "blur" or "focus" or + # any other event. + # + # Additionally, you may specify any of the options documented in + # link_to_remote. + def observe_field(field_id, options = {}) + if options[:frequency] && options[:frequency] > 0 + build_observer('Form.Element.Observer', field_id, options) + else + build_observer('Form.Element.EventObserver', field_id, options) + end + end + + # Like +observe_field+, but operates on an entire form identified by the + # DOM ID +form_id+. +options+ are the same as +observe_field+, except + # the default value of the :with option evaluates to the + # serialized (request string) value of the form. + def observe_form(form_id, options = {}) + if options[:frequency] + build_observer('Form.Observer', form_id, options) + else + build_observer('Form.EventObserver', form_id, options) + end + end + + # All the methods were moved to GeneratorMethods so that + # #include_helpers_from_context has nothing to overwrite. + class JavaScriptGenerator #:nodoc: + def initialize(context, &block) #:nodoc: + @context, @lines = context, [] + include_helpers_from_context + @context.instance_exec(self, &block) + end + + private + def include_helpers_from_context + @context.extended_by.each do |mod| + extend mod unless mod.name =~ /^ActionView::Helpers/ + end + extend GeneratorMethods + end + + # JavaScriptGenerator generates blocks of JavaScript code that allow you + # to change the content and presentation of multiple DOM elements. Use + # this in your Ajax response bodies, either in a + # + # mail_to "me@domain.com", "My email", :encode => "hex" # => + # My email + # + # You can also specify the cc address, bcc address, subject, and body parts of the message header to create a complex e-mail using the + # corresponding +cc+, +bcc+, +subject+, and +body+ html_options keys. Each of these options are URI escaped and then appended to + # the email_address before being output. Be aware that javascript keywords will not be escaped and may break this feature + # when encoding with javascript. + # Examples: + # mail_to "me@domain.com", "My email", :cc => "ccaddress@domain.com", :bcc => "bccaddress@domain.com", :subject => "This is an example email", :body => "This is the body of the message." # => + # My email + def mail_to(email_address, name = nil, html_options = {}) + html_options = html_options.stringify_keys + encode = html_options.delete("encode") + cc, bcc, subject, body = html_options.delete("cc"), html_options.delete("bcc"), html_options.delete("subject"), html_options.delete("body") + + string = '' + extras = '' + extras << "cc=#{CGI.escape(cc).gsub("+", "%20")}&" unless cc.nil? + extras << "bcc=#{CGI.escape(bcc).gsub("+", "%20")}&" unless bcc.nil? + extras << "body=#{CGI.escape(body).gsub("+", "%20")}&" unless body.nil? + extras << "subject=#{CGI.escape(subject).gsub("+", "%20")}&" unless subject.nil? + extras = "?" << extras.gsub!(/&?$/,"") unless extras.empty? + + email_address_obfuscated = email_address.dup + email_address_obfuscated.gsub!(/@/, html_options.delete("replace_at")) if html_options.has_key?("replace_at") + email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.has_key?("replace_dot") + + if encode == 'javascript' + tmp = "document.write('#{content_tag("a", name || email_address, html_options.merge({ "href" => "mailto:"+email_address.to_s+extras }))}');" + for i in 0...tmp.length + string << sprintf("%%%x",tmp[i]) + end + "" + elsif encode == 'hex' + for i in 0...email_address.length + if email_address[i,1] =~ /\w/ + string << sprintf("%%%x",email_address[i]) + else + string << email_address[i,1] + end + end + content_tag "a", name || email_address_obfuscated, html_options.merge({ "href" => "mailto:#{string}#{extras}" }) + else + content_tag "a", name || email_address_obfuscated, html_options.merge({ "href" => "mailto:#{email_address}#{extras}" }) + end + end + + # Returns true if the current page uri is generated by the options passed (in url_for format). + def current_page?(options) + CGI.escapeHTML(url_for(options)) == @controller.request.request_uri + end + + private + def convert_options_to_javascript!(html_options) + confirm, popup, post = html_options.delete("confirm"), html_options.delete("popup"), html_options.delete("post") + + html_options["onclick"] = case + when popup && post + raise ActionView::ActionViewError, "You can't use :popup and :post in the same link" + when confirm && popup + "if (#{confirm_javascript_function(confirm)}) { #{popup_javascript_function(popup)} };return false;" + when confirm && post + "if (#{confirm_javascript_function(confirm)}) { #{post_javascript_function} };return false;" + when confirm + "return #{confirm_javascript_function(confirm)};" + when post + "#{post_javascript_function}return false;" + when popup + popup_javascript_function(popup) + 'return false;' + else + html_options["onclick"] + end + end + + def confirm_javascript_function(confirm) + "confirm('#{escape_javascript(confirm)}')" + end + + def popup_javascript_function(popup) + popup.is_a?(Array) ? "window.open(this.href,'#{popup.first}','#{popup.last}');" : "window.open(this.href);" + end + + def post_javascript_function + "var f = document.createElement('form'); this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href; f.submit();" + end + + # Processes the _html_options_ hash, converting the boolean + # attributes from true/false form into the form required by + # HTML/XHTML. (An attribute is considered to be boolean if + # its name is listed in the given _bool_attrs_ array.) + # + # More specifically, for each boolean attribute in _html_options_ + # given as: + # + # "attr" => bool_value + # + # if the associated _bool_value_ evaluates to true, it is + # replaced with the attribute's name; otherwise the attribute is + # removed from the _html_options_ hash. (See the XHTML 1.0 spec, + # section 4.5 "Attribute Minimization" for more: + # http://www.w3.org/TR/xhtml1/#h-4.5) + # + # Returns the updated _html_options_ hash, which is also modified + # in place. + # + # Example: + # + # convert_boolean_attributes!( html_options, + # %w( checked disabled readonly ) ) + def convert_boolean_attributes!(html_options, bool_attrs) + bool_attrs.each { |x| html_options[x] = x if html_options.delete(x) } + html_options + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/partials.rb b/vendor/rails/actionpack/lib/action_view/partials.rb new file mode 100644 index 00000000..063ff568 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/partials.rb @@ -0,0 +1,128 @@ +module ActionView + # There's also a convenience method for rendering sub templates within the current controller that depends on a single object + # (we call this kind of sub templates for partials). It relies on the fact that partials should follow the naming convention of being + # prefixed with an underscore -- as to separate them from regular templates that could be rendered on their own. + # + # In a template for Advertiser#account: + # + # <%= render :partial => "account" %> + # + # This would render "advertiser/_account.rhtml" and pass the instance variable @account in as a local variable +account+ to + # the template for display. + # + # In another template for Advertiser#buy, we could have: + # + # <%= render :partial => "account", :locals => { :account => @buyer } %> + # + # <% for ad in @advertisements %> + # <%= render :partial => "ad", :locals => { :ad => ad } %> + # <% end %> + # + # This would first render "advertiser/_account.rhtml" with @buyer passed in as the local variable +account+, then render + # "advertiser/_ad.rhtml" and pass the local variable +ad+ to the template for display. + # + # == Rendering a collection of partials + # + # The example of partial use describes a familiar pattern where a template needs to iterate over an array and render a sub + # template for each of the elements. This pattern has been implemented as a single method that accepts an array and renders + # a partial by the same name as the elements contained within. So the three-lined example in "Using partials" can be rewritten + # with a single line: + # + # <%= render :partial => "ad", :collection => @advertisements %> + # + # This will render "advertiser/_ad.rhtml" and pass the local variable +ad+ to the template for display. An iteration counter + # will automatically be made available to the template with a name of the form +partial_name_counter+. In the case of the + # example above, the template would be fed +ad_counter+. + # + # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also just keep domain objects, + # like Active Records, in there. + # + # == Rendering shared partials + # + # Two controllers can share a set of partials and render them like this: + # + # <%= render :partial => "advertisement/ad", :locals => { :ad => @advertisement } %> + # + # This will render the partial "advertisement/_ad.rhtml" regardless of which controller this is being called from. + module Partials + # Deprecated, use render :partial + def render_partial(partial_path, local_assigns = nil, deprecated_local_assigns = nil) #:nodoc: + path, partial_name = partial_pieces(partial_path) + object = extracting_object(partial_name, local_assigns, deprecated_local_assigns) + local_assigns = extract_local_assigns(local_assigns, deprecated_local_assigns) + local_assigns = local_assigns ? local_assigns.clone : {} + add_counter_to_local_assigns!(partial_name, local_assigns) + add_object_to_local_assigns!(partial_name, local_assigns, object) + + if logger + ActionController::Base.benchmark("Rendered #{path}/_#{partial_name}", Logger::DEBUG, false) do + render("#{path}/_#{partial_name}", local_assigns) + end + else + render("#{path}/_#{partial_name}", local_assigns) + end + end + + # Deprecated, use render :partial, :collection + def render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = nil) #:nodoc: + collection_of_partials = Array.new + counter_name = partial_counter_name(partial_name) + local_assigns = local_assigns ? local_assigns.clone : {} + collection.each_with_index do |element, counter| + local_assigns[counter_name] = counter + collection_of_partials.push(render_partial(partial_name, element, local_assigns)) + end + + return " " if collection_of_partials.empty? + + if partial_spacer_template + spacer_path, spacer_name = partial_pieces(partial_spacer_template) + collection_of_partials.join(render("#{spacer_path}/_#{spacer_name}")) + else + collection_of_partials.join + end + end + + alias_method :render_collection_of_partials, :render_partial_collection + + private + def partial_pieces(partial_path) + if partial_path.include?('/') + return File.dirname(partial_path), File.basename(partial_path) + else + return controller.class.controller_path, partial_path + end + end + + def partial_counter_name(partial_name) + "#{partial_name.split('/').last}_counter".intern + end + + def extracting_object(partial_name, local_assigns, deprecated_local_assigns) + if local_assigns.is_a?(Hash) || local_assigns.nil? + controller.instance_variable_get("@#{partial_name}") + else + # deprecated form where object could be passed in as second parameter + local_assigns + end + end + + def extract_local_assigns(local_assigns, deprecated_local_assigns) + local_assigns.is_a?(Hash) ? local_assigns : deprecated_local_assigns + end + + def add_counter_to_local_assigns!(partial_name, local_assigns) + counter_name = partial_counter_name(partial_name) + local_assigns[counter_name] = 1 unless local_assigns.has_key?(counter_name) + end + + def add_object_to_local_assigns!(partial_name, local_assigns, object) + local_assigns[partial_name.intern] ||= + if object.is_a?(ActionView::Base::ObjectWrapper) + object.value + else + object + end || controller.instance_variable_get("@#{partial_name}") + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/template_error.rb b/vendor/rails/actionpack/lib/action_view/template_error.rb new file mode 100644 index 00000000..e21f7517 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/template_error.rb @@ -0,0 +1,86 @@ +module ActionView + # The TemplateError exception is raised when the compilation of the template fails. This exception then gathers a + # bunch of intimate details and uses it to report a very precise exception message. + class TemplateError < ActionViewError #:nodoc: + SOURCE_CODE_RADIUS = 3 + + attr_reader :original_exception + + def initialize(base_path, file_name, assigns, source, original_exception) + @base_path, @assigns, @source, @original_exception = + base_path, assigns, source, original_exception + @file_name = file_name + end + + def message + original_exception.message + end + + def sub_template_message + if @sub_templates + "Trace of template inclusion: " + + @sub_templates.collect { |template| strip_base_path(template) }.join(", ") + else + "" + end + end + + def source_extract(indention = 0) + source_code = IO.readlines(@file_name) + + start_on_line = [ line_number - SOURCE_CODE_RADIUS - 1, 0 ].max + end_on_line = [ line_number + SOURCE_CODE_RADIUS - 1, source_code.length].min + + line_counter = start_on_line + extract = source_code[start_on_line..end_on_line].collect do |line| + line_counter += 1 + "#{' ' * indention}#{line_counter}: " + line + end + + extract.join + end + + def sub_template_of(file_name) + @sub_templates ||= [] + @sub_templates << file_name + end + + def line_number + if file_name + regexp = /#{Regexp.escape File.basename(file_name)}:(\d+)/ + [@original_exception.message, @original_exception.clean_backtrace].flatten.each do |line| + return $1.to_i if regexp =~ line + end + end + 0 + end + + def file_name + stripped = strip_base_path(@file_name) + stripped[0] == ?/ ? stripped[1..-1] : stripped + end + + def to_s + "\n\n#{self.class} (#{message}) on line ##{line_number} of #{file_name}:\n" + + source_extract + "\n " + + original_exception.clean_backtrace.join("\n ") + + "\n\n" + end + + def backtrace + [ + "On line ##{line_number} of #{file_name}\n\n#{source_extract(4)}\n " + + original_exception.clean_backtrace.join("\n ") + ] + end + + private + def strip_base_path(file_name) + file_name = File.expand_path(file_name).gsub(/^#{Regexp.escape File.expand_path(RAILS_ROOT)}/, '') + file_name.gsub(@base_path, "") + end + end +end + +Exception::TraceSubstitutions << [/:in\s+`_run_(html|xml).*'\s*$/, ''] if defined?(Exception::TraceSubstitutions) +Exception::TraceSubstitutions << [%r{^\s*#{Regexp.escape RAILS_ROOT}}, '#{RAILS_ROOT}'] if defined?(RAILS_ROOT) diff --git a/vendor/rails/actionpack/test/abstract_unit.rb b/vendor/rails/actionpack/test/abstract_unit.rb new file mode 100644 index 00000000..94fcae4c --- /dev/null +++ b/vendor/rails/actionpack/test/abstract_unit.rb @@ -0,0 +1,14 @@ +$:.unshift(File.dirname(__FILE__) + '/../lib') +$:.unshift(File.dirname(__FILE__) + '/../../activesupport/lib/active_support') +$:.unshift(File.dirname(__FILE__) + '/fixtures/helpers') + +require 'yaml' +require 'test/unit' +require 'action_controller' +require 'breakpoint' + +require 'action_controller/test_process' + +ActionController::Base.logger = nil +ActionController::Base.ignore_missing_templates = false +ActionController::Routing::Routes.reload rescue nil \ No newline at end of file diff --git a/vendor/rails/actionpack/test/active_record_unit.rb b/vendor/rails/actionpack/test/active_record_unit.rb new file mode 100644 index 00000000..016f331d --- /dev/null +++ b/vendor/rails/actionpack/test/active_record_unit.rb @@ -0,0 +1,88 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +# Define the essentials +class ActiveRecordTestConnector + cattr_accessor :able_to_connect + cattr_accessor :connected + + # Set our defaults + self.connected = false + self.able_to_connect = true +end + +# Try to grab AR +begin + PATH_TO_AR = File.dirname(__FILE__) + '/../../activerecord' + require "#{PATH_TO_AR}/lib/active_record" unless Object.const_defined?(:ActiveRecord) + require "#{PATH_TO_AR}/lib/active_record/fixtures" unless Object.const_defined?(:Fixtures) +rescue Object => e + $stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}" + ActiveRecordTestConnector.able_to_connect = false +end + +# Define the rest of the connector +class ActiveRecordTestConnector + def self.setup + unless self.connected || !self.able_to_connect + setup_connection + load_schema + self.connected = true + end + rescue Object => e + $stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}" + #$stderr.puts " #{e.backtrace.join("\n ")}\n" + self.able_to_connect = false + end + + private + + def self.setup_connection + if Object.const_defined?(:ActiveRecord) + + begin + ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :dbfile => ':memory:') + ActiveRecord::Base.connection + rescue Object + $stderr.puts 'SQLite 3 unavailable; falling to SQLite 2.' + ActiveRecord::Base.establish_connection(:adapter => 'sqlite', :dbfile => ':memory:') + ActiveRecord::Base.connection + end + + Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE) + else + raise "Couldn't locate ActiveRecord." + end + end + + # Load actionpack sqlite tables + def self.load_schema + File.read(File.dirname(__FILE__) + "/fixtures/db_definitions/sqlite.sql").split(';').each do |sql| + ActiveRecord::Base.connection.execute(sql) unless sql.blank? + end + end +end + +# Test case for inheiritance +class ActiveRecordTestCase < Test::Unit::TestCase + # Set our fixture path + self.fixture_path = "#{File.dirname(__FILE__)}/fixtures/" + + def setup + abort_tests unless ActiveRecordTestConnector.connected = true + end + + # Default so Test::Unit::TestCase doesn't complain + def test_truth + end + + private + + # If things go wrong, we don't want to run our test cases. We'll just define them to test nothing. + def abort_tests + self.class.public_instance_methods.grep(/^test./).each do |method| + self.class.class_eval { define_method(method.to_sym){} } + end + end +end + +ActiveRecordTestConnector.setup \ No newline at end of file diff --git a/vendor/rails/actionpack/test/activerecord/active_record_assertions_test.rb b/vendor/rails/actionpack/test/activerecord/active_record_assertions_test.rb new file mode 100644 index 00000000..f5661d19 --- /dev/null +++ b/vendor/rails/actionpack/test/activerecord/active_record_assertions_test.rb @@ -0,0 +1,84 @@ +require "#{File.dirname(__FILE__)}/../active_record_unit" +require 'fixtures/company' + +class ActiveRecordAssertionsController < ActionController::Base + self.template_root = "#{File.dirname(__FILE__)}/../fixtures/" + + # fail with 1 bad column + def nasty_columns_1 + @company = Company.new + @company.name = "B" + @company.rating = 2 + render :inline => "snicker...." + end + + # fail with 2 bad columns + def nasty_columns_2 + @company = Company.new + @company.name = "" + @company.rating = 2 + render :inline => "double snicker...." + end + + # this will pass validation + def good_company + @company = Company.new + @company.name = "A" + @company.rating = 69 + render :inline => "Goodness Gracious!" + end + + # this will fail validation + def bad_company + @company = Company.new + render :inline => "Who's Bad?" + end + + # the safety dance...... + def rescue_action(e) raise; end +end + +class ActiveRecordAssertionsControllerTest < ActiveRecordTestCase + fixtures :companies + + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @controller = ActiveRecordAssertionsController.new + super + end + + # test for 1 bad apple column + def test_some_invalid_columns + process :nasty_columns_1 + assert_success + assert_invalid_record 'company' + assert_invalid_column_on_record 'company', 'rating' + assert_valid_column_on_record 'company', 'name' + assert_valid_column_on_record 'company', %w(name id) + end + + # test for 2 bad apples columns + def test_all_invalid_columns + process :nasty_columns_2 + assert_success + assert_invalid_record 'company' + assert_invalid_column_on_record 'company', 'rating' + assert_invalid_column_on_record 'company', 'name' + assert_invalid_column_on_record 'company', %w(name rating) + end + + # ensure we have no problems with an ActiveRecord + def test_valid_record + process :good_company + assert_success + assert_valid_record 'company' + end + + # ensure we have problems with an ActiveRecord + def test_invalid_record + process :bad_company + assert_success + assert_invalid_record 'company' + end +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/activerecord/active_record_store_test.rb b/vendor/rails/actionpack/test/activerecord/active_record_store_test.rb new file mode 100644 index 00000000..96c147c7 --- /dev/null +++ b/vendor/rails/actionpack/test/activerecord/active_record_store_test.rb @@ -0,0 +1,174 @@ +# Unfurl the safety net. +path_to_ar = File.dirname(__FILE__) + '/../../../activerecord' +if Object.const_defined?(:ActiveRecord) or File.exist?(path_to_ar) + begin + +# These tests exercise CGI::Session::ActiveRecordStore, so you're going to +# need AR in a sibling directory to AP and have SQLite installed. + +unless Object.const_defined?(:ActiveRecord) + require File.join(path_to_ar, 'lib', 'active_record') +end + +require File.dirname(__FILE__) + '/../abstract_unit' +require 'action_controller/session/active_record_store' + +#ActiveRecord::Base.logger = Logger.new($stdout) +begin + CGI::Session::ActiveRecordStore::Session.establish_connection(:adapter => 'sqlite3', :database => ':memory:') + CGI::Session::ActiveRecordStore::Session.connection +rescue Object + $stderr.puts 'SQLite 3 unavailable; falling back to SQLite 2.' + begin + CGI::Session::ActiveRecordStore::Session.establish_connection(:adapter => 'sqlite', :database => ':memory:') + CGI::Session::ActiveRecordStore::Session.connection + rescue Object + $stderr.puts 'SQLite 2 unavailable; skipping ActiveRecordStore test suite.' + raise SystemExit + end +end + + +module CommonActiveRecordStoreTests + def test_basics + s = session_class.new(:session_id => '1234', :data => { 'foo' => 'bar' }) + assert_equal 'bar', s.data['foo'] + assert s.save + assert_equal 'bar', s.data['foo'] + + assert_not_nil t = session_class.find_by_session_id('1234') + assert_not_nil t.data + assert_equal 'bar', t.data['foo'] + end + + def test_reload_same_session + @new_session.update + reloaded = CGI::Session.new(CGI.new, 'session_id' => @new_session.session_id, 'database_manager' => CGI::Session::ActiveRecordStore) + assert_equal 'bar', reloaded['foo'] + end + + def test_tolerates_close_close + assert_nothing_raised do + @new_session.close + @new_session.close + end + end +end + +class ActiveRecordStoreTest < Test::Unit::TestCase + include CommonActiveRecordStoreTests + + def session_class + CGI::Session::ActiveRecordStore::Session + end + + def session_id_column + "session_id" + end + + def setup + session_class.create_table! + + ENV['REQUEST_METHOD'] = 'GET' + CGI::Session::ActiveRecordStore.session_class = session_class + + @cgi = CGI.new + @new_session = CGI::Session.new(@cgi, 'database_manager' => CGI::Session::ActiveRecordStore, 'new_session' => true) + @new_session['foo'] = 'bar' + end + +# this test only applies for eager sesssion saving +# def test_another_instance +# @another = CGI::Session.new(@cgi, 'session_id' => @new_session.session_id, 'database_manager' => CGI::Session::ActiveRecordStore) +# assert_equal @new_session.session_id, @another.session_id +# end + + def test_model_attribute + assert_kind_of CGI::Session::ActiveRecordStore::Session, @new_session.model + assert_equal({ 'foo' => 'bar' }, @new_session.model.data) + end + + def test_save_unloaded_session + c = session_class.connection + bogus_class = c.quote(Base64.encode64("\004\010o:\vBlammo\000")) + c.insert("INSERT INTO #{session_class.table_name} ('#{session_id_column}', 'data') VALUES ('abcdefghijklmnop', #{bogus_class})") + + sess = session_class.find_by_session_id('abcdefghijklmnop') + assert_not_nil sess + assert !sess.loaded? + + # because the session is not loaded, the save should be a no-op. If it + # isn't, this'll try and unmarshall the bogus class, and should get an error. + assert_nothing_raised { sess.save } + end + + def teardown + session_class.drop_table! + end +end + +class ColumnLimitTest < Test::Unit::TestCase + def setup + @session_class = CGI::Session::ActiveRecordStore::Session + @session_class.create_table! + end + + def teardown + @session_class.drop_table! + end + + def test_protection_from_data_larger_than_column + # Can't test this unless there is a limit + return unless limit = @session_class.data_column_size_limit + too_big = ':(' * limit + s = @session_class.new(:session_id => '666', :data => {'foo' => too_big}) + s.data + assert_raise(ActionController::SessionOverflowError) { s.save } + end +end + +class DeprecatedActiveRecordStoreTest < ActiveRecordStoreTest + def session_id_column + "sessid" + end + + def setup + session_class.connection.execute 'create table old_sessions (id integer primary key, sessid text unique, data text)' + session_class.table_name = 'old_sessions' + session_class.send :setup_sessid_compatibility! + + ENV['REQUEST_METHOD'] = 'GET' + CGI::Session::ActiveRecordStore.session_class = session_class + + @new_session = CGI::Session.new(CGI.new, 'database_manager' => CGI::Session::ActiveRecordStore, 'new_session' => true) + @new_session['foo'] = 'bar' + end + + def teardown + session_class.connection.execute 'drop table old_sessions' + session_class.table_name = 'sessions' + end +end + +class SqlBypassActiveRecordStoreTest < ActiveRecordStoreTest + def session_class + unless @session_class + @session_class = CGI::Session::ActiveRecordStore::SqlBypass + @session_class.connection = CGI::Session::ActiveRecordStore::Session.connection + end + @session_class + end + + def test_model_attribute + assert_kind_of CGI::Session::ActiveRecordStore::SqlBypass, @new_session.model + assert_equal({ 'foo' => 'bar' }, @new_session.model.data) + end +end + + +# End of safety net. + rescue Object => e + $stderr.puts "Skipping CGI::Session::ActiveRecordStore tests: #{e}" + #$stderr.puts " #{e.backtrace.join("\n ")}" + end +end diff --git a/vendor/rails/actionpack/test/activerecord/pagination_test.rb b/vendor/rails/actionpack/test/activerecord/pagination_test.rb new file mode 100644 index 00000000..386300c0 --- /dev/null +++ b/vendor/rails/actionpack/test/activerecord/pagination_test.rb @@ -0,0 +1,161 @@ +require File.dirname(__FILE__) + '/../active_record_unit' + +require 'fixtures/topic' +require 'fixtures/reply' +require 'fixtures/developer' +require 'fixtures/project' + +class PaginationTest < ActiveRecordTestCase + fixtures :topics, :replies, :developers, :projects, :developers_projects + + class PaginationController < ActionController::Base + self.template_root = "#{File.dirname(__FILE__)}/../fixtures/" + + def simple_paginate + @topic_pages, @topics = paginate(:topics) + render :nothing => true + end + + def paginate_with_per_page + @topic_pages, @topics = paginate(:topics, :per_page => 1) + render :nothing => true + end + + def paginate_with_order + @topic_pages, @topics = paginate(:topics, :order => 'created_at asc') + render :nothing => true + end + + def paginate_with_order_by + @topic_pages, @topics = paginate(:topics, :order_by => 'created_at asc') + render :nothing => true + end + + def paginate_with_include_and_order + @topic_pages, @topics = paginate(:topics, :include => :replies, :order => 'replies.created_at asc, topics.created_at asc') + render :nothing => true + end + + def paginate_with_conditions + @topic_pages, @topics = paginate(:topics, :conditions => ["created_at > ?", 30.minutes.ago]) + render :nothing => true + end + + def paginate_with_class_name + @developer_pages, @developers = paginate(:developers, :class_name => "DeVeLoPeR") + render :nothing => true + end + + def paginate_with_singular_name + @developer_pages, @developers = paginate() + render :nothing => true + end + + def paginate_with_joins + @developer_pages, @developers = paginate(:developers, + :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', + :conditions => 'project_id=1') + render :nothing => true + end + + def paginate_with_join + @developer_pages, @developers = paginate(:developers, + :join => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', + :conditions => 'project_id=1') + render :nothing => true + end + + def paginate_with_join_and_count + @developer_pages, @developers = paginate(:developers, + :join => 'd LEFT JOIN developers_projects ON d.id = developers_projects.developer_id', + :conditions => 'project_id=1', + :count => "d.id") + render :nothing => true + end + + def rescue_errors(e) raise e end + + def rescue_action(e) raise end + + end + + def setup + @controller = PaginationController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + super + end + + # Single Action Pagination Tests + + def test_simple_paginate + get :simple_paginate + assert_equal 1, assigns(:topic_pages).page_count + assert_equal 3, assigns(:topics).size + end + + def test_paginate_with_per_page + get :paginate_with_per_page + assert_equal 1, assigns(:topics).size + assert_equal 3, assigns(:topic_pages).page_count + end + + def test_paginate_with_order + get :paginate_with_order + expected = [topics(:futurama), + topics(:harvey_birdman), + topics(:rails)] + assert_equal expected, assigns(:topics) + assert_equal 1, assigns(:topic_pages).page_count + end + + def test_paginate_with_order_by + get :paginate_with_order + expected = assigns(:topics) + get :paginate_with_order_by + assert_equal expected, assigns(:topics) + assert_equal 1, assigns(:topic_pages).page_count + end + + def test_paginate_with_conditions + get :paginate_with_conditions + expected = [topics(:rails)] + assert_equal expected, assigns(:topics) + assert_equal 1, assigns(:topic_pages).page_count + end + + def test_paginate_with_class_name + get :paginate_with_class_name + + assert assigns(:developers).size > 0 + assert_equal DeVeLoPeR, assigns(:developers).first.class + end + + def test_paginate_with_joins + get :paginate_with_joins + assert_equal 2, assigns(:developers).size + developer_names = assigns(:developers).map { |d| d.name } + assert developer_names.include?('David') + assert developer_names.include?('Jamis') + end + + def test_paginate_with_join_and_conditions + get :paginate_with_joins + expected = assigns(:developers) + get :paginate_with_join + assert_equal expected, assigns(:developers) + end + + def test_paginate_with_join_and_count + get :paginate_with_joins + expected = assigns(:developers) + get :paginate_with_join_and_count + assert_equal expected, assigns(:developers) + end + + def test_paginate_with_include_and_order + get :paginate_with_include_and_order + expected = Topic.find(:all, :include => 'replies', :order => 'replies.created_at asc, topics.created_at asc', :limit => 10) + assert_equal expected, assigns(:topics) + end +end diff --git a/vendor/rails/actionpack/test/controller/action_pack_assertions_test.rb b/vendor/rails/actionpack/test/controller/action_pack_assertions_test.rb new file mode 100644 index 00000000..b359750d --- /dev/null +++ b/vendor/rails/actionpack/test/controller/action_pack_assertions_test.rb @@ -0,0 +1,493 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +# a controller class to facilitate the tests +class ActionPackAssertionsController < ActionController::Base + + # this does absolutely nothing + def nothing() render_text ""; end + + # a standard template + def hello_world() render "test/hello_world"; end + + # a standard template + def hello_xml_world() render "test/hello_xml_world"; end + + # a redirect to an internal location + def redirect_internal() redirect_to "/nothing"; end + + def redirect_to_action() redirect_to :action => "flash_me", :id => 1, :params => { "panda" => "fun" }; end + + def redirect_to_controller() redirect_to :controller => "elsewhere", :action => "flash_me"; end + + def redirect_to_path() redirect_to '/some/path' end + + def redirect_to_named_route() redirect_to route_one_url end + + # a redirect to an external location + def redirect_external() redirect_to_url "http://www.rubyonrails.org"; end + + # a 404 + def response404() render_text "", "404 AWOL"; end + + # a 500 + def response500() render_text "", "500 Sorry"; end + + # a fictional 599 + def response599() render_text "", "599 Whoah!"; end + + # putting stuff in the flash + def flash_me + flash['hello'] = 'my name is inigo montoya...' + render_text "Inconceivable!" + end + + # we have a flash, but nothing is in it + def flash_me_naked + flash.clear + render_text "wow!" + end + + # assign some template instance variables + def assign_this + @howdy = "ho" + render :inline => "Mr. Henke" + end + + def render_based_on_parameters + render_text "Mr. #{@params["name"]}" + end + + def render_url + render_text "
      #{url_for(:action => 'flash_me', :only_path => true)}
      " + end + + # puts something in the session + def session_stuffing + session['xmas'] = 'turkey' + render_text "ho ho ho" + end + + # raises exception on get requests + def raise_on_get + raise "get" if @request.get? + render_text "request method: #{@request.env['REQUEST_METHOD']}" + end + + # raises exception on post requests + def raise_on_post + raise "post" if @request.post? + render_text "request method: #{@request.env['REQUEST_METHOD']}" + end + + def get_valid_record + @record = Class.new do + def valid? + true + end + + def errors + Class.new do + def full_messages; []; end + end.new + end + + end.new + + render :nothing => true + end + + + def get_invalid_record + @record = Class.new do + + def valid? + false + end + + def errors + Class.new do + def full_messages; ['...stuff...']; end + end.new + end + end.new + + render :nothing => true + end + + # 911 + def rescue_action(e) raise; end +end + +module Admin + class InnerModuleController < ActionController::Base + def redirect_to_absolute_controller + redirect_to :controller => '/content' + end + def redirect_to_fellow_controller + redirect_to :controller => 'user' + end + end +end + +# --------------------------------------------------------------------------- + + +# tell the controller where to find its templates but start from parent +# directory of test_request_response to simulate the behaviour of a +# production environment +ActionPackAssertionsController.template_root = File.dirname(__FILE__) + "/../fixtures/" + + +# a test case to exercise the new capabilities TestRequest & TestResponse +class ActionPackAssertionsControllerTest < Test::Unit::TestCase + # let's get this party started + def setup + @controller = ActionPackAssertionsController.new + @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new + end + + # -- assertion-based testing ------------------------------------------------ + + def test_assert_tag_and_url_for + get :render_url + assert_tag :content => "/action_pack_assertions/flash_me" + end + + # test the session assertion to make sure something is there. + def test_assert_session_has + process :session_stuffing + assert_session_has 'xmas' + assert_session_has_no 'halloween' + end + + # test the get method, make sure the request really was a get + def test_get + assert_raise(RuntimeError) { get :raise_on_get } + get :raise_on_post + assert_equal @response.body, 'request method: GET' + end + + # test the get method, make sure the request really was a get + def test_post + assert_raise(RuntimeError) { post :raise_on_post } + post :raise_on_get + assert_equal @response.body, 'request method: POST' + end + +# the following test fails because the request_method is now cached on the request instance +# test the get/post switch within one test action +# def test_get_post_switch +# post :raise_on_get +# assert_equal @response.body, 'request method: POST' +# get :raise_on_post +# assert_equal @response.body, 'request method: GET' +# post :raise_on_get +# assert_equal @response.body, 'request method: POST' +# get :raise_on_post +# assert_equal @response.body, 'request method: GET' +# end + + # test the assertion of goodies in the template + def test_assert_template_has + process :assign_this + assert_template_has 'howdy' + end + + # test the assertion for goodies that shouldn't exist in the template + def test_assert_template_has_no + process :nothing + assert_template_has_no 'maple syrup' + assert_template_has_no 'howdy' + end + + # test the redirection assertions + def test_assert_redirect + process :redirect_internal + assert_redirect + end + + # test the redirect url string + def test_assert_redirect_url + process :redirect_external + assert_redirect_url 'http://www.rubyonrails.org' + end + + # test the redirection pattern matching on a string + def test_assert_redirect_url_match_string + process :redirect_external + assert_redirect_url_match 'rails.org' + end + + # test the redirection pattern matching on a pattern + def test_assert_redirect_url_match_pattern + process :redirect_external + assert_redirect_url_match /ruby/ + end + + # test the redirection to a named route + def test_assert_redirect_to_named_route + process :redirect_to_named_route + assert_raise(Test::Unit::AssertionFailedError) do + assert_redirected_to 'http://test.host/route_two' + end + end + + # test the flash-based assertions with something is in the flash + def test_flash_assertions_full + process :flash_me + assert @response.has_flash_with_contents? + assert_flash_exists + assert_flash_not_empty + assert_flash_has 'hello' + assert_flash_has_no 'stds' + end + + # test the flash-based assertions with no flash at all + def test_flash_assertions_negative + process :nothing + assert_flash_empty + assert_flash_has_no 'hello' + assert_flash_has_no 'qwerty' + end + + # test the assert_rendered_file + def test_assert_rendered_file + process :hello_world + assert_rendered_file 'test/hello_world' + assert_rendered_file 'hello_world' + end + + # test the assert_success assertion + def test_assert_success + process :nothing + assert_success + assert_rendered_file + end + + # -- standard request/response object testing -------------------------------- + + # ensure our session is working properly + def test_session_objects + process :session_stuffing + assert @response.has_session_object?('xmas') + assert_session_equal 'turkey', 'xmas' + assert !@response.has_session_object?('easter') + end + + # make sure that the template objects exist + def test_template_objects_alive + process :assign_this + assert !@response.has_template_object?('hi') + assert @response.has_template_object?('howdy') + end + + # make sure we don't have template objects when we shouldn't + def test_template_object_missing + process :nothing + assert_nil @response.template_objects['howdy'] + end + + def test_assigned_equal + process :assign_this + assert_assigned_equal "ho", :howdy + end + + # check the empty flashing + def test_flash_me_naked + process :flash_me_naked + assert !@response.has_flash? + assert !@response.has_flash_with_contents? + end + + # check if we have flash objects + def test_flash_haves + process :flash_me + assert @response.has_flash? + assert @response.has_flash_with_contents? + assert @response.has_flash_object?('hello') + end + + # ensure we don't have flash objects + def test_flash_have_nots + process :nothing + assert !@response.has_flash? + assert !@response.has_flash_with_contents? + assert_nil @response.flash['hello'] + end + + # examine that the flash objects are what we expect + def test_flash_equals + process :flash_me + assert_flash_equal 'my name is inigo montoya...', 'hello' + end + + + # check if we were rendered by a file-based template? + def test_rendered_action + process :nothing + assert !@response.rendered_with_file? + + process :hello_world + assert @response.rendered_with_file? + assert 'hello_world', @response.rendered_file + end + + # check the redirection location + def test_redirection_location + process :redirect_internal + assert_equal 'http://test.host/nothing', @response.redirect_url + + process :redirect_external + assert_equal 'http://www.rubyonrails.org', @response.redirect_url + + process :nothing + assert_nil @response.redirect_url + end + + + # check server errors + def test_server_error_response_code + process :response500 + assert @response.server_error? + + process :response599 + assert @response.server_error? + + process :response404 + assert !@response.server_error? + end + + # check a 404 response code + def test_missing_response_code + process :response404 + assert @response.missing? + end + + # check to see if our redirection matches a pattern + def test_redirect_url_match + process :redirect_external + assert @response.redirect? + assert @response.redirect_url_match?("rubyonrails") + assert @response.redirect_url_match?(/rubyonrails/) + assert !@response.redirect_url_match?("phpoffrails") + assert !@response.redirect_url_match?(/perloffrails/) + end + + # check for a redirection + def test_redirection + process :redirect_internal + assert @response.redirect? + + process :redirect_external + assert @response.redirect? + + process :nothing + assert !@response.redirect? + end + + # check a successful response code + def test_successful_response_code + process :nothing + assert @response.success? + end + + # a basic check to make sure we have a TestResponse object + def test_has_response + process :nothing + assert_kind_of ActionController::TestResponse, @response + end + + def test_render_based_on_parameters + process :render_based_on_parameters, "name" => "David" + assert_equal "Mr. David", @response.body + end + + def test_assert_template_xpath_match_no_matches + process :hello_xml_world + assert_raises Test::Unit::AssertionFailedError do + assert_template_xpath_match('/no/such/node/in/document') + end + end + + def test_simple_one_element_xpath_match + process :hello_xml_world + assert_template_xpath_match('//title', "Hello World") + end + + def test_array_of_elements_in_xpath_match + process :hello_xml_world + assert_template_xpath_match('//p', %w( abes monks wiseguys )) + end + + def test_follow_redirect + process :redirect_to_action + assert_redirected_to :action => "flash_me" + + follow_redirect + assert_equal 1, @request.parameters["id"].to_i + + assert "Inconceivable!", @response.body + end + + def test_follow_redirect_outside_current_action + process :redirect_to_controller + assert_redirected_to :controller => "elsewhere", :action => "flash_me" + + assert_raises(RuntimeError, "Can't follow redirects outside of current controller (elsewhere)") { follow_redirect } + end + + def test_redirected_to_url_leadling_slash + process :redirect_to_path + assert_redirected_to '/some/path' + end + def test_redirected_to_url_no_leadling_slash + process :redirect_to_path + assert_redirected_to 'some/path' + end + def test_redirected_to_url_full_url + process :redirect_to_path + assert_redirected_to 'http://test.host/some/path' + end + + def test_redirected_to_with_nested_controller + @controller = Admin::InnerModuleController.new + get :redirect_to_absolute_controller + assert_redirected_to :controller => 'content' + + get :redirect_to_fellow_controller + assert_redirected_to :controller => 'admin/user' + end + + def test_assert_valid + get :get_valid_record + assert_valid assigns('record') + end + + def test_assert_valid_failing + get :get_invalid_record + + begin + assert_valid assigns('record') + assert false + rescue Test::Unit::AssertionFailedError => e + end + end +end + +class ActionPackHeaderTest < Test::Unit::TestCase + def setup + @controller = ActionPackAssertionsController.new + @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new + end + + def test_rendering_xml_sets_content_type + process :hello_xml_world + assert_equal('application/xml', @controller.headers['Content-Type']) + end + + def test_rendering_xml_respects_content_type + @response.headers['Content-Type'] = 'application/pdf' + process :hello_xml_world + assert_equal('application/pdf', @controller.headers['Content-Type']) + end +end diff --git a/vendor/rails/actionpack/test/controller/addresses_render_test.rb b/vendor/rails/actionpack/test/controller/addresses_render_test.rb new file mode 100644 index 00000000..d85b6f5c --- /dev/null +++ b/vendor/rails/actionpack/test/controller/addresses_render_test.rb @@ -0,0 +1,49 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class Address + + def Address.count(conditions = nil, join = nil) + nil + end + + def Address.find_all(arg1, arg2, arg3, arg4) + [] + end + + def self.find(*args) + [] + end +end + +class AddressesTestController < ActionController::Base + + scaffold :address + + def self.controller_name; "addresses"; end + def self.controller_path; "addresses"; end + +end + +AddressesTestController.template_root = File.dirname(__FILE__) + "/../fixtures/" + +class AddressesTest < Test::Unit::TestCase + def setup + @controller = AddressesTestController.new + + # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get + # a more accurate simulation of what happens in "real life". + @controller.logger = Logger.new(nil) + + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + + @request.host = "www.nextangle.com" + end + + def test_list + get :list + assert_equal "We only need to get this far!", @response.body.chomp + end + + +end diff --git a/vendor/rails/actionpack/test/controller/base_test.rb b/vendor/rails/actionpack/test/controller/base_test.rb new file mode 100644 index 00000000..10a3e0ce --- /dev/null +++ b/vendor/rails/actionpack/test/controller/base_test.rb @@ -0,0 +1,66 @@ +require File.dirname(__FILE__) + '/../abstract_unit' +require 'test/unit' +require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late + +# Provide some controller to run the tests on. +module Submodule + class ContainedEmptyController < ActionController::Base + end + class ContainedNonEmptyController < ActionController::Base + def public_action + end + + hide_action :hidden_action + def hidden_action + end + + def another_hidden_action + end + hide_action :another_hidden_action + end + class SubclassedController < ContainedNonEmptyController + hide_action :public_action # Hiding it here should not affect the superclass. + end +end +class EmptyController < ActionController::Base + include ActionController::Caching +end +class NonEmptyController < ActionController::Base + def public_action + end + + hide_action :hidden_action + def hidden_action + end +end + +class ControllerClassTests < Test::Unit::TestCase + def test_controller_path + assert_equal 'empty', EmptyController.controller_path + assert_equal 'submodule/contained_empty', Submodule::ContainedEmptyController.controller_path + end + def test_controller_name + assert_equal 'empty', EmptyController.controller_name + assert_equal 'contained_empty', Submodule::ContainedEmptyController.controller_name + end +end + +class ControllerInstanceTests < Test::Unit::TestCase + def setup + @empty = EmptyController.new + @contained = Submodule::ContainedEmptyController.new + @empty_controllers = [@empty, @contained, Submodule::SubclassedController.new] + + @non_empty_controllers = [NonEmptyController.new, + Submodule::ContainedNonEmptyController.new] + end + + def test_action_methods + @empty_controllers.each do |c| + assert_equal Set.new, c.send(:action_methods), "#{c.class.controller_path} should be empty!" + end + @non_empty_controllers.each do |c| + assert_equal Set.new('public_action'), c.send(:action_methods), "#{c.class.controller_path} should not be empty!" + end + end +end diff --git a/vendor/rails/actionpack/test/controller/benchmark_test.rb b/vendor/rails/actionpack/test/controller/benchmark_test.rb new file mode 100644 index 00000000..f346e575 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/benchmark_test.rb @@ -0,0 +1,33 @@ +require File.dirname(__FILE__) + '/../abstract_unit' +require 'test/unit' + +# Provide some static controllers. +class BenchmarkedController < ActionController::Base + def public_action + render :nothing => true + end + + def rescue_action(e) + raise e + end +end + +class BenchmarkTest < Test::Unit::TestCase + class MockLogger + def method_missing(*args) + end + end + + def setup + @controller = BenchmarkedController.new + # benchmark doesn't do anything unless a logger is set + @controller.logger = MockLogger.new + @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new + @request.host = "test.actioncontroller.i" + end + + def test_with_http_1_0_request + @request.host = nil + assert_nothing_raised { get :public_action } + end +end diff --git a/vendor/rails/actionpack/test/controller/caching_filestore.rb b/vendor/rails/actionpack/test/controller/caching_filestore.rb new file mode 100644 index 00000000..389ebe02 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/caching_filestore.rb @@ -0,0 +1,74 @@ +require 'fileutils' +require File.dirname(__FILE__) + '/../abstract_unit' + +class TestLogDevice < Logger::LogDevice + attr :last_message, true + + def initialize + @last_message=String.new + end + + def write(message) + @last_message << message + end + + def clear + @last_message = String.new + end +end + +#setup our really sophisticated logger +TestLog = TestLogDevice.new +RAILS_DEFAULT_LOGGER = Logger.new(TestLog) +ActionController::Base.logger = RAILS_DEFAULT_LOGGER + +def use_store + #generate a random key to ensure the cache is always in a different location + RANDOM_KEY = rand(99999999).to_s + FILE_STORE_PATH = File.dirname(__FILE__) + '/../temp/' + RANDOM_KEY + ActionController::Base.perform_caching = true + ActionController::Base.fragment_cache_store = :file_store, FILE_STORE_PATH +end + +class TestController < ActionController::Base + caches_action :render_to_cache, :index + + def render_to_cache + render_text "Render Cached" + end + alias :index :render_to_cache +end + +class FileStoreTest < Test::Unit::TestCase + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @controller = TestController.new + @request.host = "hostname.com" + end + + def teardown + FileUtils.rm_rf(FILE_STORE_PATH) + end + + def test_render_cached + assert_fragment_cached { get :render_to_cache } + assert_fragment_hit { get :render_to_cache } + end + + + private + def assert_fragment_cached + yield + assert(TestLog.last_message.include?("Cached fragment:"), "--ERROR-- FileStore write failed ----") + assert(!TestLog.last_message.include?("Couldn't create cache directory:"), "--ERROR-- FileStore create directory failed ----") + TestLog.clear + end + + def assert_fragment_hit + yield + assert(TestLog.last_message.include?("Fragment read:"), "--ERROR-- Fragment not found in FileStore ----") + assert(!TestLog.last_message.include?("Cached fragment:"), "--ERROR-- Did cache ----") + TestLog.clear + end +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/controller/capture_test.rb b/vendor/rails/actionpack/test/controller/capture_test.rb new file mode 100644 index 00000000..e4f76a4d --- /dev/null +++ b/vendor/rails/actionpack/test/controller/capture_test.rb @@ -0,0 +1,80 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class CaptureController < ActionController::Base + def self.controller_name; "test"; end + def self.controller_path; "test"; end + + def content_for + render :layout => "talk_from_action" + end + + def erb_content_for + render :layout => "talk_from_action" + end + + def block_content_for + render :layout => "talk_from_action" + end + + def non_erb_block_content_for + render :layout => "talk_from_action" + end + + def rescue_action(e) raise end +end + +CaptureController.template_root = File.dirname(__FILE__) + "/../fixtures/" + +class CaptureTest < Test::Unit::TestCase + def setup + @controller = CaptureController.new + + # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get + # a more accurate simulation of what happens in "real life". + @controller.logger = Logger.new(nil) + + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + + @request.host = "www.nextangle.com" + end + + def test_simple_capture + get :capturing + assert_equal "Dreamy days", @response.body.strip + end + + def test_content_for + get :content_for + assert_equal expected_content_for_output, @response.body + end + + def test_erb_content_for + get :content_for + assert_equal expected_content_for_output, @response.body + end + + def test_block_content_for + get :block_content_for + assert_equal expected_content_for_output, @response.body + end + + def test_non_erb_block_content_for + get :non_erb_block_content_for + assert_equal expected_content_for_output, @response.body + end + + def test_update_element_with_capture + get :update_element_with_capture + assert_equal( + "" + + "\n\n$('status').innerHTML = '\\n You bought something!\\n';", + @response.body.strip + ) + end + + private + def expected_content_for_output + "Putting stuff in the title!\n\nGreat stuff!" + end +end diff --git a/vendor/rails/actionpack/test/controller/cgi_test.rb b/vendor/rails/actionpack/test/controller/cgi_test.rb new file mode 100755 index 00000000..ddf68fdf --- /dev/null +++ b/vendor/rails/actionpack/test/controller/cgi_test.rb @@ -0,0 +1,363 @@ +require File.dirname(__FILE__) + '/../abstract_unit' +require 'action_controller/cgi_process' +require 'action_controller/cgi_ext/cgi_ext' + + +require 'stringio' + +class CGITest < Test::Unit::TestCase + def setup + @query_string = "action=create_customer&full_name=David%20Heinemeier%20Hansson&customerId=1" + @query_string_with_nil = "action=create_customer&full_name=" + @query_string_with_array = "action=create_customer&selected[]=1&selected[]=2&selected[]=3" + @query_string_with_amps = "action=create_customer&name=Don%27t+%26+Does" + @query_string_with_multiple_of_same_name = + "action=update_order&full_name=Lau%20Taarnskov&products=4&products=2&products=3" + @query_string_with_many_equal = "action=create_customer&full_name=abc=def=ghi" + @query_string_without_equal = "action" + @query_string_with_many_ampersands = + "&action=create_customer&&&full_name=David%20Heinemeier%20Hansson" + end + + def test_query_string + assert_equal( + { "action" => "create_customer", "full_name" => "David Heinemeier Hansson", "customerId" => "1"}, + CGIMethods.parse_query_parameters(@query_string) + ) + end + + def test_deep_query_string + assert_equal({'x' => {'y' => {'z' => '10'}}}, CGIMethods.parse_query_parameters('x[y][z]=10')) + end + + def test_deep_query_string_with_array + assert_equal({'x' => {'y' => {'z' => ['10']}}}, CGIMethods.parse_query_parameters('x[y][z][]=10')) + assert_equal({'x' => {'y' => {'z' => ['10', '5']}}}, CGIMethods.parse_query_parameters('x[y][z][]=10&x[y][z][]=5')) + end + + def test_query_string_with_nil + assert_equal( + { "action" => "create_customer", "full_name" => nil}, + CGIMethods.parse_query_parameters(@query_string_with_nil) + ) + end + + def test_query_string_with_array + assert_equal( + { "action" => "create_customer", "selected" => ["1", "2", "3"]}, + CGIMethods.parse_query_parameters(@query_string_with_array) + ) + end + + def test_query_string_with_amps + assert_equal( + { "action" => "create_customer", "name" => "Don't & Does"}, + CGIMethods.parse_query_parameters(@query_string_with_amps) + ) + end + + def test_query_string_with_many_equal + assert_equal( + { "action" => "create_customer", "full_name" => "abc=def=ghi"}, + CGIMethods.parse_query_parameters(@query_string_with_many_equal) + ) + end + + def test_query_string_without_equal + assert_equal( + { "action" => nil }, + CGIMethods.parse_query_parameters(@query_string_without_equal) + ) + end + + def test_query_string_with_many_ampersands + assert_equal( + { "action" => "create_customer", "full_name" => "David Heinemeier Hansson"}, + CGIMethods.parse_query_parameters(@query_string_with_many_ampersands) + ) + end + + def test_parse_params + input = { + "customers[boston][first][name]" => [ "David" ], + "customers[boston][first][url]" => [ "http://David" ], + "customers[boston][second][name]" => [ "Allan" ], + "customers[boston][second][url]" => [ "http://Allan" ], + "something_else" => [ "blah" ], + "something_nil" => [ nil ], + "something_empty" => [ "" ], + "products[first]" => [ "Apple Computer" ], + "products[second]" => [ "Pc" ] + } + + expected_output = { + "customers" => { + "boston" => { + "first" => { + "name" => "David", + "url" => "http://David" + }, + "second" => { + "name" => "Allan", + "url" => "http://Allan" + } + } + }, + "something_else" => "blah", + "something_empty" => "", + "something_nil" => "", + "products" => { + "first" => "Apple Computer", + "second" => "Pc" + } + } + + assert_equal expected_output, CGIMethods.parse_request_parameters(input) + end + + def test_parse_params_from_multipart_upload + mockup = Struct.new(:content_type, :original_filename) + file = mockup.new('img/jpeg', 'foo.jpg') + ie_file = mockup.new('img/jpeg', 'c:\\Documents and Settings\\foo\\Desktop\\bar.jpg') + + input = { + "something" => [ StringIO.new("") ], + "array_of_stringios" => [[ StringIO.new("One"), StringIO.new("Two") ]], + "mixed_types_array" => [[ StringIO.new("Three"), "NotStringIO" ]], + "mixed_types_as_checkboxes[strings][nested]" => [[ file, "String", StringIO.new("StringIO")]], + "ie_mixed_types_as_checkboxes[strings][nested]" => [[ ie_file, "String", StringIO.new("StringIO")]], + "products[string]" => [ StringIO.new("Apple Computer") ], + "products[file]" => [ file ], + "ie_products[string]" => [ StringIO.new("Microsoft") ], + "ie_products[file]" => [ ie_file ] + } + + expected_output = { + "something" => "", + "array_of_stringios" => ["One", "Two"], + "mixed_types_array" => [ "Three", "NotStringIO" ], + "mixed_types_as_checkboxes" => { + "strings" => { + "nested" => [ file, "String", "StringIO" ] + }, + }, + "ie_mixed_types_as_checkboxes" => { + "strings" => { + "nested" => [ ie_file, "String", "StringIO" ] + }, + }, + "products" => { + "string" => "Apple Computer", + "file" => file + }, + "ie_products" => { + "string" => "Microsoft", + "file" => ie_file + } + } + + params = CGIMethods.parse_request_parameters(input) + assert_equal expected_output, params + + # Lone filenames are preserved. + assert_equal 'foo.jpg', params['mixed_types_as_checkboxes']['strings']['nested'].first.original_filename + assert_equal 'foo.jpg', params['products']['file'].original_filename + + # But full Windows paths are reduced to their basename. + assert_equal 'bar.jpg', params['ie_mixed_types_as_checkboxes']['strings']['nested'].first.original_filename + assert_equal 'bar.jpg', params['ie_products']['file'].original_filename + end + + def test_parse_params_with_file + input = { + "customers[boston][first][name]" => [ "David" ], + "something_else" => [ "blah" ], + "logo" => [ File.new(File.dirname(__FILE__) + "/cgi_test.rb").path ] + } + + expected_output = { + "customers" => { + "boston" => { + "first" => { + "name" => "David" + } + } + }, + "something_else" => "blah", + "logo" => File.new(File.dirname(__FILE__) + "/cgi_test.rb").path, + } + + assert_equal expected_output, CGIMethods.parse_request_parameters(input) + end + + def test_parse_params_with_array + input = { "selected[]" => [ "1", "2", "3" ] } + + expected_output = { "selected" => [ "1", "2", "3" ] } + + assert_equal expected_output, CGIMethods.parse_request_parameters(input) + end + + def test_parse_params_with_non_alphanumeric_name + input = { "a/b[c]" => %w(d) } + expected = { "a/b" => { "c" => "d" }} + assert_equal expected, CGIMethods.parse_request_parameters(input) + end + + def test_parse_params_with_single_brackets_in_middle + input = { "a/b[c]d" => %w(e) } + expected = { "a/b[c]d" => "e" } + assert_equal expected, CGIMethods.parse_request_parameters(input) + end + + def test_parse_params_with_separated_brackets + input = { "a/b@[c]d[e]" => %w(f) } + expected = { "a/b@" => { "c]d[e" => "f" }} + assert_equal expected, CGIMethods.parse_request_parameters(input) + end + + def test_parse_params_with_separated_brackets_and_array + input = { "a/b@[c]d[e][]" => %w(f) } + expected = { "a/b@" => { "c]d[e" => ["f"] }} + assert_equal expected , CGIMethods.parse_request_parameters(input) + end + + def test_parse_params_with_unmatched_brackets_and_array + input = { "a/b@[c][d[e][]" => %w(f) } + expected = { "a/b@" => { "c" => { "d[e" => ["f"] }}} + assert_equal expected, CGIMethods.parse_request_parameters(input) + end +end + +class MultipartCGITest < Test::Unit::TestCase + FIXTURE_PATH = File.dirname(__FILE__) + '/../fixtures/multipart' + + def setup + ENV['REQUEST_METHOD'] = 'POST' + ENV['CONTENT_LENGTH'] = '0' + ENV['CONTENT_TYPE'] = 'multipart/form-data, boundary=AaB03x' + end + + def test_single_parameter + params = process('single_parameter') + assert_equal({ 'foo' => 'bar' }, params) + end + + def test_text_file + params = process('text_file') + assert_equal %w(file foo), params.keys.sort + assert_equal 'bar', params['foo'] + + file = params['file'] + assert_kind_of StringIO, file + assert_equal 'file.txt', file.original_filename + assert_equal "text/plain\r", file.content_type + assert_equal 'contents', file.read + end + + def test_large_text_file + params = process('large_text_file') + assert_equal %w(file foo), params.keys.sort + assert_equal 'bar', params['foo'] + + file = params['file'] + assert_kind_of Tempfile, file + assert_equal 'file.txt', file.original_filename + assert_equal "text/plain\r", file.content_type + assert ('a' * 20480) == file.read + end + + def test_binary_file + params = process('binary_file') + assert_equal %w(file flowers foo), params.keys.sort + assert_equal 'bar', params['foo'] + + file = params['file'] + assert_kind_of StringIO, file + assert_equal 'file.txt', file.original_filename + assert_equal "text/plain\r", file.content_type + assert_equal 'contents', file.read + + file = params['flowers'] + assert_kind_of StringIO, file + assert_equal 'flowers.jpg', file.original_filename + assert_equal "image/jpeg\r", file.content_type + assert_equal 19512, file.size + #assert_equal File.read(File.dirname(__FILE__) + '/../../../activerecord/test/fixtures/flowers.jpg'), file.read + end + + def test_mixed_files + params = process('mixed_files') + assert_equal %w(files foo), params.keys.sort + assert_equal 'bar', params['foo'] + + # Ruby CGI doesn't handle multipart/mixed for us. + assert_kind_of StringIO, params['files'] + assert_equal 19756, params['files'].size + end + + private + def process(name) + old_stdin = $stdin + File.open(File.join(FIXTURE_PATH, name), 'rb') do |file| + ENV['CONTENT_LENGTH'] = file.stat.size.to_s + $stdin = file + CGIMethods.parse_request_parameters CGI.new.params + end + ensure + $stdin = old_stdin + end +end + + +class CGIRequestTest < Test::Unit::TestCase + def setup + @request_hash = {"HTTP_MAX_FORWARDS"=>"10", "SERVER_NAME"=>"glu.ttono.us:8007", "FCGI_ROLE"=>"RESPONDER", "HTTP_X_FORWARDED_HOST"=>"glu.ttono.us", "HTTP_ACCEPT_ENCODING"=>"gzip, deflate", "HTTP_USER_AGENT"=>"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1", "PATH_INFO"=>"", "HTTP_ACCEPT_LANGUAGE"=>"en", "HTTP_HOST"=>"glu.ttono.us:8007", "SERVER_PROTOCOL"=>"HTTP/1.1", "REDIRECT_URI"=>"/dispatch.fcgi", "SCRIPT_NAME"=>"/dispatch.fcgi", "SERVER_ADDR"=>"207.7.108.53", "REMOTE_ADDR"=>"207.7.108.53", "SERVER_SOFTWARE"=>"lighttpd/1.4.5", "HTTP_COOKIE"=>"_session_id=c84ace84796670c052c6ceb2451fb0f2; is_admin=yes", "HTTP_X_FORWARDED_SERVER"=>"glu.ttono.us", "REQUEST_URI"=>"/admin", "DOCUMENT_ROOT"=>"/home/kevinc/sites/typo/public", "SERVER_PORT"=>"8007", "QUERY_STRING"=>"", "REMOTE_PORT"=>"63137", "GATEWAY_INTERFACE"=>"CGI/1.1", "HTTP_X_FORWARDED_FOR"=>"65.88.180.234", "HTTP_ACCEPT"=>"*/*", "SCRIPT_FILENAME"=>"/home/kevinc/sites/typo/public/dispatch.fcgi", "REDIRECT_STATUS"=>"200", "REQUEST_METHOD"=>"GET"} + # cookie as returned by some Nokia phone browsers (no space after semicolon separator) + @alt_cookie_fmt_request_hash = {"HTTP_COOKIE"=>"_session_id=c84ace84796670c052c6ceb2451fb0f2;is_admin=yes"} + @fake_cgi = Struct.new(:env_table).new(@request_hash) + @request = ActionController::CgiRequest.new(@fake_cgi) + end + + def test_proxy_request + assert_equal 'glu.ttono.us', @request.host_with_port + end + + def test_http_host + @request_hash.delete "HTTP_X_FORWARDED_HOST" + @request_hash['HTTP_HOST'] = "rubyonrails.org:8080" + assert_equal "rubyonrails.org:8080", @request.host_with_port + + @request_hash['HTTP_X_FORWARDED_HOST'] = "www.firsthost.org, www.secondhost.org" + assert_equal "www.secondhost.org", @request.host + end + + def test_http_host_with_default_port_overrides_server_port + @request_hash.delete "HTTP_X_FORWARDED_HOST" + @request_hash['HTTP_HOST'] = "rubyonrails.org" + assert_equal "rubyonrails.org", @request.host_with_port + end + + def test_host_with_port_defaults_to_server_name_if_no_host_headers + @request_hash.delete "HTTP_X_FORWARDED_HOST" + @request_hash.delete "HTTP_HOST" + assert_equal "glu.ttono.us:8007", @request.host_with_port + end + + def test_host_with_port_falls_back_to_server_addr_if_necessary + @request_hash.delete "HTTP_X_FORWARDED_HOST" + @request_hash.delete "HTTP_HOST" + @request_hash.delete "SERVER_NAME" + assert_equal "207.7.108.53:8007", @request.host_with_port + end + + def test_cookie_syntax_resilience + cookies = CGI::Cookie::parse(@request_hash["HTTP_COOKIE"]); + assert_equal ["c84ace84796670c052c6ceb2451fb0f2"], cookies["_session_id"] + assert_equal ["yes"], cookies["is_admin"] + + alt_cookies = CGI::Cookie::parse(@alt_cookie_fmt_request_hash["HTTP_COOKIE"]); + assert_equal ["c84ace84796670c052c6ceb2451fb0f2"], alt_cookies["_session_id"] + assert_equal ["yes"], alt_cookies["is_admin"] + end +end diff --git a/vendor/rails/actionpack/test/controller/components_test.rb b/vendor/rails/actionpack/test/controller/components_test.rb new file mode 100644 index 00000000..d10f7102 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/components_test.rb @@ -0,0 +1,129 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class CallerController < ActionController::Base + def calling_from_controller + render_component(:controller => "callee", :action => "being_called") + end + + def calling_from_controller_with_params + render_component(:controller => "callee", :action => "being_called", :params => { "name" => "David" }) + end + + def calling_from_controller_with_different_status_code + render_component(:controller => "callee", :action => "blowing_up") + end + + def calling_from_template + render_template "Ring, ring: <%= render_component(:controller => 'callee', :action => 'being_called') %>" + end + + def internal_caller + render_template "Are you there? <%= render_component(:action => 'internal_callee') %>" + end + + def internal_callee + render_text "Yes, ma'am" + end + + def set_flash + render_component(:controller => "callee", :action => "set_flash") + end + + def use_flash + render_component(:controller => "callee", :action => "use_flash") + end + + def calling_redirected + render_component(:controller => "callee", :action => "redirected") + end + + def calling_redirected_as_string + render_template "<%= render_component(:controller => 'callee', :action => 'redirected') %>" + end + + def rescue_action(e) raise end +end + +class CalleeController < ActionController::Base + def being_called + render_text "#{@params["name"] || "Lady"} of the House, speaking" + end + + def blowing_up + render_text "It's game over, man, just game over, man!", "500 Internal Server Error" + end + + def set_flash + flash[:notice] = 'My stoney baby' + render :text => 'flash is set' + end + + def use_flash + render :text => flash[:notice] || 'no flash' + end + + def redirected + redirect_to :controller => "callee", :action => "being_called" + end + + def rescue_action(e) raise end +end + +class ComponentsTest < Test::Unit::TestCase + def setup + @controller = CallerController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_calling_from_controller + get :calling_from_controller + assert_equal "Lady of the House, speaking", @response.body + end + + def test_calling_from_controller_with_params + get :calling_from_controller_with_params + assert_equal "David of the House, speaking", @response.body + end + + def test_calling_from_controller_with_different_status_code + get :calling_from_controller_with_different_status_code + assert_equal 500, @response.response_code + end + + def test_calling_from_template + get :calling_from_template + assert_equal "Ring, ring: Lady of the House, speaking", @response.body + end + + def test_internal_calling + get :internal_caller + assert_equal "Are you there? Yes, ma'am", @response.body + end + + def test_flash + get :set_flash + assert_equal 'My stoney baby', flash[:notice] + get :use_flash + assert_equal 'My stoney baby', @response.body + get :use_flash + assert_equal 'no flash', @response.body + end + + def test_component_redirect_redirects + get :calling_redirected + + assert_redirected_to :action => "being_called" + end + + def test_component_multiple_redirect_redirects + test_component_redirect_redirects + test_internal_calling + end + + def test_component_as_string_redirect_renders_redirecte_action + get :calling_redirected_as_string + + assert_equal "Lady of the House, speaking", @response.body + end +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/controller/cookie_test.rb b/vendor/rails/actionpack/test/controller/cookie_test.rb new file mode 100644 index 00000000..f0189ebb --- /dev/null +++ b/vendor/rails/actionpack/test/controller/cookie_test.rb @@ -0,0 +1,80 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class CookieTest < Test::Unit::TestCase + class TestController < ActionController::Base + def authenticate_with_deprecated_writer + cookie "name" => "user_name", "value" => "david" + render_text "hello world" + end + + def authenticate + cookies["user_name"] = "david" + render_text "hello world" + end + + def authenticate_for_fourten_days + cookies["user_name"] = { "value" => "david", "expires" => Time.local(2005, 10, 10) } + render_text "hello world" + end + + def authenticate_for_fourten_days_with_symbols + cookies[:user_name] = { :value => "david", :expires => Time.local(2005, 10, 10) } + render_text "hello world" + end + + def set_multiple_cookies + cookies["user_name"] = { "value" => "david", "expires" => Time.local(2005, 10, 10) } + cookies["login"] = "XJ-122" + render_text "hello world" + end + + def access_frozen_cookies + @cookies["will"] = "work" + render_text "hello world" + end + + def rescue_action(e) raise end + end + + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + + @request.host = "www.nextangle.com" + end + + def test_setting_cookie_with_deprecated_writer + @request.action = "authenticate_with_deprecated_writer" + assert_equal [ CGI::Cookie::new("name" => "user_name", "value" => "david") ], process_request.headers["cookie"] + end + + def test_setting_cookie + @request.action = "authenticate" + assert_equal [ CGI::Cookie::new("name" => "user_name", "value" => "david") ], process_request.headers["cookie"] + end + + def test_setting_cookie_for_fourteen_days + @request.action = "authenticate_for_fourten_days" + assert_equal [ CGI::Cookie::new("name" => "user_name", "value" => "david", "expires" => Time.local(2005, 10, 10)) ], process_request.headers["cookie"] + end + + def test_setting_cookie_for_fourteen_days_with_symbols + @request.action = "authenticate_for_fourten_days" + assert_equal [ CGI::Cookie::new("name" => "user_name", "value" => "david", "expires" => Time.local(2005, 10, 10)) ], process_request.headers["cookie"] + end + + def test_multiple_cookies + @request.action = "set_multiple_cookies" + assert_equal 2, process_request.headers["cookie"].size + end + + def test_setting_test_cookie + @request.action = "access_frozen_cookies" + assert_nothing_raised { process_request } + end + + private + def process_request + TestController.process(@request, @response) + end +end diff --git a/vendor/rails/actionpack/test/controller/custom_handler_test.rb b/vendor/rails/actionpack/test/controller/custom_handler_test.rb new file mode 100644 index 00000000..2747a0f3 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/custom_handler_test.rb @@ -0,0 +1,41 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class CustomHandler + def initialize( view ) + @view = view + end + + def render( template, local_assigns ) + [ template, + local_assigns, + @view ] + end +end + +class CustomHandlerTest < Test::Unit::TestCase + def setup + ActionView::Base.register_template_handler "foo", CustomHandler + ActionView::Base.register_template_handler :foo2, CustomHandler + @view = ActionView::Base.new + end + + def test_custom_render + result = @view.render_template( "foo", "hello <%= one %>", nil, :one => "two" ) + assert_equal( + [ "hello <%= one %>", { :one => "two" }, @view ], + result ) + end + + def test_custom_render2 + result = @view.render_template( "foo2", "hello <%= one %>", nil, :one => "two" ) + assert_equal( + [ "hello <%= one %>", { :one => "two" }, @view ], + result ) + end + + def test_unhandled_extension + # uses the ERb handler by default if the extension isn't recognized + result = @view.render_template( "bar", "hello <%= one %>", nil, :one => "two" ) + assert_equal "hello two", result + end +end diff --git a/vendor/rails/actionpack/test/controller/fake_controllers.rb b/vendor/rails/actionpack/test/controller/fake_controllers.rb new file mode 100644 index 00000000..f95339a2 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/fake_controllers.rb @@ -0,0 +1,17 @@ +class << Object; alias_method :const_available?, :const_defined?; end + +class ContentController < Class.new(ActionController::Base) +end +class NotAController +end +module Admin + class << self; alias_method :const_available?, :const_defined?; end + SomeConstant = 10 + class UserController < Class.new(ActionController::Base); end + class NewsFeedController < Class.new(ActionController::Base); end +end + +ActionController::Routing::Routes.draw do |map| + map.route_one 'route_one', :controller => 'elsewhere', :action => 'flash_me' + map.connect ':controller/:action/:id' +end diff --git a/vendor/rails/actionpack/test/controller/filter_params_test.rb b/vendor/rails/actionpack/test/controller/filter_params_test.rb new file mode 100644 index 00000000..5ad0d7f8 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/filter_params_test.rb @@ -0,0 +1,42 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class FilterParamController < ActionController::Base +end + +class FilterParamTest < Test::Unit::TestCase + def setup + @controller = FilterParamController.new + end + + def test_filter_parameters + assert FilterParamController.respond_to?(:filter_parameter_logging) + assert !@controller.respond_to?(:filter_parameters) + + FilterParamController.filter_parameter_logging + assert @controller.respond_to?(:filter_parameters) + + test_hashes = [[{},{},[]], + [{'foo'=>'bar'},{'foo'=>'bar'},[]], + [{'foo'=>'bar'},{'foo'=>'bar'},%w'food'], + [{'foo'=>'bar'},{'foo'=>'[FILTERED]'},%w'foo'], + [{'foo'=>'bar', 'bar'=>'foo'},{'foo'=>'[FILTERED]', 'bar'=>'foo'},%w'foo baz'], + [{'foo'=>'bar', 'baz'=>'foo'},{'foo'=>'[FILTERED]', 'baz'=>'[FILTERED]'},%w'foo baz'], + [{'bar'=>{'foo'=>'bar','bar'=>'foo'}},{'bar'=>{'foo'=>'[FILTERED]','bar'=>'foo'}},%w'fo'], + [{'foo'=>{'foo'=>'bar','bar'=>'foo'}},{'foo'=>'[FILTERED]'},%w'f banana']] + + test_hashes.each do |before_filter, after_filter, filter_words| + FilterParamController.filter_parameter_logging(*filter_words) + assert_equal after_filter, @controller.filter_parameters(before_filter) + + filter_words.push('blah') + FilterParamController.filter_parameter_logging(*filter_words) do |key, value| + value.reverse! if key =~ /bargain/ + end + + before_filter['barg'] = {'bargain'=>'gain', 'blah'=>'bar', 'bar'=>{'bargain'=>{'blah'=>'foo'}}} + after_filter['barg'] = {'bargain'=>'niag', 'blah'=>'[FILTERED]', 'bar'=>{'bargain'=>{'blah'=>'[FILTERED]'}}} + + assert_equal after_filter, @controller.filter_parameters(before_filter) + end + end +end diff --git a/vendor/rails/actionpack/test/controller/filters_test.rb b/vendor/rails/actionpack/test/controller/filters_test.rb new file mode 100644 index 00000000..d92143e4 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/filters_test.rb @@ -0,0 +1,410 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class FilterTest < Test::Unit::TestCase + class TestController < ActionController::Base + before_filter :ensure_login + after_filter :clean_up + + def show + render :inline => "ran action" + end + + private + def ensure_login + @ran_filter ||= [] + @ran_filter << "ensure_login" + end + + def clean_up + @ran_after_filter ||= [] + @ran_after_filter << "clean_up" + end + end + + class RenderingController < ActionController::Base + before_filter :render_something_else + + def show + @ran_action = true + render :inline => "ran action" + end + + private + def render_something_else + render :inline => "something else" + end + end + + class ConditionalFilterController < ActionController::Base + def show + render :inline => "ran action" + end + + def another_action + render :inline => "ran action" + end + + def show_without_filter + render :inline => "ran action without filter" + end + + private + def ensure_login + @ran_filter ||= [] + @ran_filter << "ensure_login" + end + + def clean_up_tmp + @ran_filter ||= [] + @ran_filter << "clean_up_tmp" + end + + def rescue_action(e) raise(e) end + end + + class ConditionalCollectionFilterController < ConditionalFilterController + before_filter :ensure_login, :except => [ :show_without_filter, :another_action ] + end + + class OnlyConditionSymController < ConditionalFilterController + before_filter :ensure_login, :only => :show + end + + class ExceptConditionSymController < ConditionalFilterController + before_filter :ensure_login, :except => :show_without_filter + end + + class BeforeAndAfterConditionController < ConditionalFilterController + before_filter :ensure_login, :only => :show + after_filter :clean_up_tmp, :only => :show + end + + class OnlyConditionProcController < ConditionalFilterController + before_filter(:only => :show) {|c| c.assigns["ran_proc_filter"] = true } + end + + class ExceptConditionProcController < ConditionalFilterController + before_filter(:except => :show_without_filter) {|c| c.assigns["ran_proc_filter"] = true } + end + + class ConditionalClassFilter + def self.filter(controller) controller.assigns["ran_class_filter"] = true end + end + + class OnlyConditionClassController < ConditionalFilterController + before_filter ConditionalClassFilter, :only => :show + end + + class ExceptConditionClassController < ConditionalFilterController + before_filter ConditionalClassFilter, :except => :show_without_filter + end + + class AnomolousYetValidConditionController < ConditionalFilterController + 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 + + class PrependingController < TestController + prepend_before_filter :wonderful_life + # skip_before_filter :fire_flash + + private + def wonderful_life + @ran_filter ||= [] + @ran_filter << "wonderful_life" + end + end + + class ConditionalSkippingController < TestController + skip_before_filter :ensure_login, :only => [ :login ] + skip_after_filter :clean_up, :only => [ :login ] + + before_filter :find_user, :only => [ :change_password ] + + def login + render :inline => "ran action" + end + + def change_password + render :inline => "ran action" + end + + protected + def find_user + @ran_filter ||= [] + @ran_filter << "find_user" + end + end + + class ConditionalParentOfConditionalSkippingController < ConditionalFilterController + before_filter :conditional_in_parent, :only => [:show, :another_action] + after_filter :conditional_in_parent, :only => [:show, :another_action] + + private + + def conditional_in_parent + @ran_filter ||= [] + @ran_filter << 'conditional_in_parent' + end + end + + class ChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController + skip_before_filter :conditional_in_parent, :only => :another_action + skip_after_filter :conditional_in_parent, :only => :another_action + end + + class ProcController < PrependingController + before_filter(proc { |c| c.assigns["ran_proc_filter"] = true }) + end + + class ImplicitProcController < PrependingController + before_filter { |c| c.assigns["ran_proc_filter"] = true } + end + + class AuditFilter + def self.filter(controller) + controller.assigns["was_audited"] = true + end + end + + class AroundFilter + def before(controller) + @execution_log = "before" + controller.class.execution_log << " before aroundfilter " if controller.respond_to? :execution_log + controller.assigns["before_ran"] = true + end + + def after(controller) + controller.assigns["execution_log"] = @execution_log + " and after" + controller.assigns["after_ran"] = true + controller.class.execution_log << " after aroundfilter " if controller.respond_to? :execution_log + end + end + + class AppendedAroundFilter + def before(controller) + controller.class.execution_log << " before appended aroundfilter " + end + + def after(controller) + controller.class.execution_log << " after appended aroundfilter " + end + end + + class AuditController < ActionController::Base + before_filter(AuditFilter) + + def show + render_text "hello" + end + end + + class BadFilterController < ActionController::Base + before_filter 2 + + def show() "show" end + + protected + def rescue_action(e) raise(e) end + end + + class AroundFilterController < PrependingController + around_filter AroundFilter.new + end + + class MixedFilterController < PrependingController + cattr_accessor :execution_log + + def initialize + @@execution_log = "" + end + + before_filter { |c| c.class.execution_log << " before procfilter " } + prepend_around_filter AroundFilter.new + + after_filter { |c| c.class.execution_log << " after procfilter " } + append_around_filter AppendedAroundFilter.new + end + + class MixedSpecializationController < ActionController::Base + class OutOfOrder < StandardError; end + + before_filter :first + before_filter :second, :only => :foo + + def foo + render_text 'foo' + end + + def bar + render_text 'bar' + end + + protected + def first + @first = true + end + + def second + raise OutOfOrder unless @first + end + end + + class DynamicDispatchController < ActionController::Base + before_filter :choose + + %w(foo bar baz).each do |action| + define_method(action) { render :text => action } + end + + private + def choose + self.action_name = params[:choose] + end + end + + def test_added_filter_to_inheritance_graph + assert_equal [ :ensure_login ], TestController.before_filters + end + + def test_base_class_in_isolation + assert_equal [ ], ActionController::Base.before_filters + end + + def test_prepending_filter + assert_equal [ :wonderful_life, :ensure_login ], PrependingController.before_filters + end + + def test_running_filters + assert_equal %w( wonderful_life ensure_login ), test_process(PrependingController).template.assigns["ran_filter"] + end + + def test_running_filters_with_proc + assert test_process(ProcController).template.assigns["ran_proc_filter"] + end + + def test_running_filters_with_implicit_proc + assert test_process(ImplicitProcController).template.assigns["ran_proc_filter"] + end + + def test_running_filters_with_class + assert test_process(AuditController).template.assigns["was_audited"] + end + + def test_running_anomolous_yet_valid_condition_filters + response = test_process(AnomolousYetValidConditionController) + assert_equal %w( ensure_login ), response.template.assigns["ran_filter"] + assert response.template.assigns["ran_class_filter"] + assert response.template.assigns["ran_proc_filter1"] + assert response.template.assigns["ran_proc_filter2"] + + response = test_process(AnomolousYetValidConditionController, "show_without_filter") + assert_equal nil, response.template.assigns["ran_filter"] + assert !response.template.assigns["ran_class_filter"] + assert !response.template.assigns["ran_proc_filter1"] + assert !response.template.assigns["ran_proc_filter2"] + end + + def test_running_collection_condition_filters + assert_equal %w( ensure_login ), test_process(ConditionalCollectionFilterController).template.assigns["ran_filter"] + assert_equal nil, test_process(ConditionalCollectionFilterController, "show_without_filter").template.assigns["ran_filter"] + assert_equal nil, test_process(ConditionalCollectionFilterController, "another_action").template.assigns["ran_filter"] + end + + def test_running_only_condition_filters + assert_equal %w( ensure_login ), test_process(OnlyConditionSymController).template.assigns["ran_filter"] + assert_equal nil, test_process(OnlyConditionSymController, "show_without_filter").template.assigns["ran_filter"] + + assert test_process(OnlyConditionProcController).template.assigns["ran_proc_filter"] + assert !test_process(OnlyConditionProcController, "show_without_filter").template.assigns["ran_proc_filter"] + + assert test_process(OnlyConditionClassController).template.assigns["ran_class_filter"] + assert !test_process(OnlyConditionClassController, "show_without_filter").template.assigns["ran_class_filter"] + end + + def test_running_except_condition_filters + assert_equal %w( ensure_login ), test_process(ExceptConditionSymController).template.assigns["ran_filter"] + assert_equal nil, test_process(ExceptConditionSymController, "show_without_filter").template.assigns["ran_filter"] + + assert test_process(ExceptConditionProcController).template.assigns["ran_proc_filter"] + assert !test_process(ExceptConditionProcController, "show_without_filter").template.assigns["ran_proc_filter"] + + assert test_process(ExceptConditionClassController).template.assigns["ran_class_filter"] + assert !test_process(ExceptConditionClassController, "show_without_filter").template.assigns["ran_class_filter"] + end + + def test_running_before_and_after_condition_filters + assert_equal %w( ensure_login clean_up_tmp), test_process(BeforeAndAfterConditionController).template.assigns["ran_filter"] + assert_equal nil, test_process(BeforeAndAfterConditionController, "show_without_filter").template.assigns["ran_filter"] + end + + def test_bad_filter + assert_raises(ActionController::ActionControllerError) { + test_process(BadFilterController) + } + end + + def test_around_filter + controller = test_process(AroundFilterController) + assert controller.template.assigns["before_ran"] + assert controller.template.assigns["after_ran"] + end + + def test_having_properties_in_around_filter + controller = test_process(AroundFilterController) + assert_equal "before and after", controller.template.assigns["execution_log"] + end + + def test_prepending_and_appending_around_filter + controller = test_process(MixedFilterController) + assert_equal " before aroundfilter before procfilter before appended aroundfilter " + + " after appended aroundfilter after aroundfilter after procfilter ", + MixedFilterController.execution_log + end + + def test_rendering_breaks_filtering_chain + response = test_process(RenderingController) + assert_equal "something else", response.body + assert !response.template.assigns["ran_action"] + end + + def test_filters_with_mixed_specialization_run_in_order + assert_nothing_raised do + response = test_process(MixedSpecializationController, 'bar') + assert_equal 'bar', response.body + end + + assert_nothing_raised do + response = test_process(MixedSpecializationController, 'foo') + assert_equal 'foo', response.body + end + end + + def test_dynamic_dispatch + %w(foo bar baz).each do |action| + request = ActionController::TestRequest.new + request.query_parameters[:choose] = action + response = DynamicDispatchController.process(request, ActionController::TestResponse.new) + assert_equal action, response.body + end + end + + def test_conditional_skipping_of_filters + 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_nil test_process(ConditionalSkippingController, "login").template.controller.instance_variable_get("@ran_after_filter") + assert_equal %w( clean_up ), test_process(ConditionalSkippingController, "change_password").template.controller.instance_variable_get("@ran_after_filter") + end + + def test_conditional_skipping_of_filters_when_parent_filter_is_also_conditional + assert_equal %w( conditional_in_parent conditional_in_parent ), test_process(ChildOfConditionalParentController).template.assigns['ran_filter'] + assert_nil test_process(ChildOfConditionalParentController, 'another_action').template.assigns['ran_filter'] + end + + private + def test_process(controller, action = "show") + request = ActionController::TestRequest.new + request.action = action + controller.process(request, ActionController::TestResponse.new) + end +end diff --git a/vendor/rails/actionpack/test/controller/flash_test.rb b/vendor/rails/actionpack/test/controller/flash_test.rb new file mode 100644 index 00000000..53d765ef --- /dev/null +++ b/vendor/rails/actionpack/test/controller/flash_test.rb @@ -0,0 +1,85 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class FlashTest < Test::Unit::TestCase + class TestController < ActionController::Base + def set_flash + flash["that"] = "hello" + render :inline => "hello" + end + + def set_flash_now + flash.now["that"] = "hello" + flash.now["foo"] ||= "bar" + flash.now["foo"] ||= "err" + @flashy = flash.now["that"] + @flash_copy = {}.update flash + render :inline => "hello" + end + + def attempt_to_use_flash_now + @flash_copy = {}.update flash + @flashy = flash["that"] + render :inline => "hello" + end + + def use_flash + @flash_copy = {}.update flash + @flashy = flash["that"] + render :inline => "hello" + end + + def use_flash_and_keep_it + @flash_copy = {}.update flash + @flashy = flash["that"] + silence_warnings { keep_flash } + render :inline => "hello" + end + + def rescue_action(e) + raise unless ActionController::MissingTemplate === e + end + end + + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @controller = TestController.new + end + + def test_flash + get :set_flash + + get :use_flash + assert_equal "hello", @response.template.assigns["flash_copy"]["that"] + assert_equal "hello", @response.template.assigns["flashy"] + + get :use_flash + assert_nil @response.template.assigns["flash_copy"]["that"], "On second flash" + end + + def test_keep_flash + get :set_flash + + get :use_flash_and_keep_it + assert_equal "hello", @response.template.assigns["flash_copy"]["that"] + assert_equal "hello", @response.template.assigns["flashy"] + + get :use_flash + assert_equal "hello", @response.template.assigns["flash_copy"]["that"], "On second flash" + + get :use_flash + assert_nil @response.template.assigns["flash_copy"]["that"], "On third flash" + end + + def test_flash_now + get :set_flash_now + assert_equal "hello", @response.template.assigns["flash_copy"]["that"] + assert_equal "bar" , @response.template.assigns["flash_copy"]["foo"] + assert_equal "hello", @response.template.assigns["flashy"] + + get :attempt_to_use_flash_now + assert_nil @response.template.assigns["flash_copy"]["that"] + assert_nil @response.template.assigns["flash_copy"]["foo"] + assert_nil @response.template.assigns["flashy"] + end +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/controller/fragment_store_setting_test.rb b/vendor/rails/actionpack/test/controller/fragment_store_setting_test.rb new file mode 100644 index 00000000..cb872f65 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/fragment_store_setting_test.rb @@ -0,0 +1,45 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +MemCache = Struct.new(:MemCache, :address) unless Object.const_defined?(:MemCache) + +class FragmentCacheStoreSettingTest < Test::Unit::TestCase + def teardown + ActionController::Base.fragment_cache_store = ActionController::Caching::Fragments::MemoryStore.new + end + + def test_file_fragment_cache_store + ActionController::Base.fragment_cache_store = :file_store, "/path/to/cache/directory" + assert_kind_of( + ActionController::Caching::Fragments::FileStore, + ActionController::Base.fragment_cache_store + ) + assert_equal "/path/to/cache/directory", ActionController::Base.fragment_cache_store.cache_path + end + + def test_drb_fragment_cache_store + ActionController::Base.fragment_cache_store = :drb_store, "druby://localhost:9192" + assert_kind_of( + ActionController::Caching::Fragments::DRbStore, + ActionController::Base.fragment_cache_store + ) + assert_equal "druby://localhost:9192", ActionController::Base.fragment_cache_store.address + end + + def test_mem_cache_fragment_cache_store + ActionController::Base.fragment_cache_store = :mem_cache_store, "localhost" + assert_kind_of( + ActionController::Caching::Fragments::MemCacheStore, + ActionController::Base.fragment_cache_store + ) + assert_equal %w(localhost), ActionController::Base.fragment_cache_store.addresses + end + + def test_object_assigned_fragment_cache_store + ActionController::Base.fragment_cache_store = ActionController::Caching::Fragments::FileStore.new("/path/to/cache/directory") + assert_kind_of( + ActionController::Caching::Fragments::FileStore, + ActionController::Base.fragment_cache_store + ) + assert_equal "/path/to/cache/directory", ActionController::Base.fragment_cache_store.cache_path + end +end diff --git a/vendor/rails/actionpack/test/controller/helper_test.rb b/vendor/rails/actionpack/test/controller/helper_test.rb new file mode 100644 index 00000000..6fb67ee3 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/helper_test.rb @@ -0,0 +1,187 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class TestController < ActionController::Base + attr_accessor :delegate_attr + def delegate_method() end + def rescue_action(e) raise end +end + +module Fun + class GamesController < ActionController::Base + def render_hello_world + render :inline => "hello: <%= stratego %>" + end + + def rescue_action(e) raise end + end + + class PDFController < ActionController::Base + def test + render :inline => "test: <%= foobar %>" + end + + def rescue_action(e) raise end + end +end + +module LocalAbcHelper + def a() end + def b() end + def c() end +end + +class HelperTest < Test::Unit::TestCase + def setup + # Increment symbol counter. + @symbol = (@@counter ||= 'A0').succ!.dup + + # Generate new controller class. + controller_class_name = "Helper#{@symbol}Controller" + eval("class #{controller_class_name} < TestController; end") + @controller_class = self.class.const_get(controller_class_name) + + # Generate new template class and assign to controller. + template_class_name = "Test#{@symbol}View" + eval("class #{template_class_name} < ActionView::Base; end") + @template_class = self.class.const_get(template_class_name) + @controller_class.template_class = @template_class + + # Set default test helper. + self.test_helper = LocalAbcHelper + end + + def teardown + # Reset template class. + #ActionController::Base.template_class = ActionView::Base + end + + + def test_deprecated_helper + assert_equal expected_helper_methods, missing_methods + assert_nothing_raised { @controller_class.helper TestHelper } + assert_equal [], missing_methods + end + + def test_declare_helper + require 'abc_helper' + self.test_helper = AbcHelper + assert_equal expected_helper_methods, missing_methods + assert_nothing_raised { @controller_class.helper :abc } + assert_equal [], missing_methods + end + + def test_declare_missing_helper + assert_equal expected_helper_methods, missing_methods + assert_raise(MissingSourceFile) { @controller_class.helper :missing } + end + + def test_declare_missing_file_from_helper + require 'broken_helper' + rescue LoadError => e + assert_nil /\bbroken_helper\b/.match(e.to_s)[1] + end + + def test_helper_block + assert_nothing_raised { + @controller_class.helper { def block_helper_method; end } + } + assert master_helper_methods.include?('block_helper_method') + end + + def test_helper_block_include + assert_equal expected_helper_methods, missing_methods + assert_nothing_raised { + @controller_class.helper { include TestHelper } + } + assert [], missing_methods + end + + def test_helper_method + assert_nothing_raised { @controller_class.helper_method :delegate_method } + assert master_helper_methods.include?('delegate_method') + end + + def test_helper_attr + assert_nothing_raised { @controller_class.helper_attr :delegate_attr } + assert master_helper_methods.include?('delegate_attr') + assert master_helper_methods.include?('delegate_attr=') + end + + def test_helper_for_nested_controller + request = ActionController::TestRequest.new + response = ActionController::TestResponse.new + request.action = 'render_hello_world' + + assert_equal 'hello: Iz guuut!', Fun::GamesController.process(request, response).body + end + + def test_helper_for_acronym_controller + request = ActionController::TestRequest.new + response = ActionController::TestResponse.new + request.action = 'test' + + assert_equal 'test: baz', Fun::PDFController.process(request, response).body + end + + private + def expected_helper_methods + TestHelper.instance_methods + end + + def master_helper_methods + @controller_class.master_helper_module.instance_methods + end + + def missing_methods + expected_helper_methods - master_helper_methods + end + + def test_helper=(helper_module) + silence_warnings { self.class.const_set('TestHelper', helper_module) } + end +end + + +class IsolatedHelpersTest < Test::Unit::TestCase + class A < ActionController::Base + def index + render :inline => '<%= shout %>' + end + + def rescue_action(e) raise end + end + + class B < A + helper { def shout; 'B' end } + + def index + render :inline => '<%= shout %>' + end + end + + class C < A + helper { def shout; 'C' end } + + def index + render :inline => '<%= shout %>' + end + end + + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @request.action = 'index' + end + + def test_helper_in_a + assert_raise(NameError) { A.process(@request, @response) } + end + + def test_helper_in_b + assert_equal 'B', B.process(@request, @response).body + end + + def test_helper_in_c + assert_equal 'C', C.process(@request, @response).body + end +end diff --git a/vendor/rails/actionpack/test/controller/layout_test.rb b/vendor/rails/actionpack/test/controller/layout_test.rb new file mode 100644 index 00000000..c7db19cc --- /dev/null +++ b/vendor/rails/actionpack/test/controller/layout_test.rb @@ -0,0 +1,73 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +# The template_root must be set on Base and not LayoutTest so that LayoutTest's inherited method has access to +# the template_root when looking for a layout +ActionController::Base.template_root = File.dirname(__FILE__) + '/../fixtures/layout_tests/' + +class LayoutTest < ActionController::Base + def self.controller_path; 'views' end +end + +# Restore template root to be unset +ActionController::Base.template_root = nil + +class ProductController < LayoutTest +end + +class ItemController < LayoutTest +end + +class ThirdPartyTemplateLibraryController < LayoutTest +end + +module ControllerNameSpace +end + +class ControllerNameSpace::NestedController < LayoutTest +end + +class MabView + def initialize(view) + end + + def render(text, locals = {}) + text + end +end + +ActionView::Base::register_template_handler :mab, MabView + +class LayoutAutoDiscoveryTest < Test::Unit::TestCase + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + + @request.host = "www.nextangle.com" + end + + def test_application_layout_is_default_when_no_controller_match + @controller = ProductController.new + get :hello + assert_equal 'layout_test.rhtml hello.rhtml', @response.body + end + + def test_controller_name_layout_name_match + @controller = ItemController.new + get :hello + assert_equal 'item.rhtml hello.rhtml', @response.body + end + + def test_third_party_template_library_auto_discovers_layout + @controller = ThirdPartyTemplateLibraryController.new + get :hello + assert_equal 'layouts/third_party_template_library', @controller.active_layout + assert_equal 'Mab', @response.body + end + + def test_namespaced_controllers_auto_detect_layouts + @controller = ControllerNameSpace::NestedController.new + get :hello + assert_equal 'layouts/controller_name_space/nested', @controller.active_layout + assert_equal 'controller_name_space/nested.rhtml hello.rhtml', @response.body + end +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/controller/mime_responds_test.rb b/vendor/rails/actionpack/test/controller/mime_responds_test.rb new file mode 100644 index 00000000..e696a41c --- /dev/null +++ b/vendor/rails/actionpack/test/controller/mime_responds_test.rb @@ -0,0 +1,257 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class RespondToController < ActionController::Base + layout :set_layout + + def html_xml_or_rss + respond_to do |type| + type.html { render :text => "HTML" } + type.xml { render :text => "XML" } + type.rss { render :text => "RSS" } + type.all { render :text => "Nothing" } + end + end + + def js_or_html + respond_to do |type| + type.html { render :text => "HTML" } + type.js { render :text => "JS" } + type.all { render :text => "Nothing" } + end + end + + def html_or_xml + respond_to do |type| + type.html { render :text => "HTML" } + type.xml { render :text => "XML" } + type.all { render :text => "Nothing" } + end + end + + def just_xml + respond_to do |type| + type.xml { render :text => "XML" } + end + end + + def using_defaults + respond_to do |type| + type.html + type.js + type.xml + end + end + + def using_defaults_with_type_list + respond_to(:html, :js, :xml) + end + + def made_for_content_type + respond_to do |type| + type.rss { render :text => "RSS" } + type.atom { render :text => "ATOM" } + type.all { render :text => "Nothing" } + end + end + + def custom_type_handling + respond_to do |type| + type.html { render :text => "HTML" } + type.custom("application/crazy-xml") { render :text => "Crazy XML" } + type.all { render :text => "Nothing" } + end + end + + def handle_any + respond_to do |type| + type.html { render :text => "HTML" } + type.any(:js, :xml) { render :text => "Either JS or XML" } + end + end + + def all_types_with_layout + respond_to do |type| + type.html + type.js + end + end + + def rescue_action(e) + raise + end + + protected + def set_layout + if action_name == "all_types_with_layout" + "standard" + end + end +end + +RespondToController.template_root = File.dirname(__FILE__) + "/../fixtures/" + +class MimeControllerTest < Test::Unit::TestCase + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + + @controller = RespondToController.new + @request.host = "www.example.com" + end + + def test_html + @request.env["HTTP_ACCEPT"] = "text/html" + get :js_or_html + assert_equal 'HTML', @response.body + + get :html_or_xml + assert_equal 'HTML', @response.body + + get :just_xml + assert_response 406 + end + + def test_all + @request.env["HTTP_ACCEPT"] = "*/*" + get :js_or_html + assert_equal 'HTML', @response.body # js is not part of all + + get :html_or_xml + assert_equal 'HTML', @response.body + + get :just_xml + assert_equal 'XML', @response.body + end + + def test_xml + @request.env["HTTP_ACCEPT"] = "application/xml" + get :html_xml_or_rss + assert_equal 'XML', @response.body + end + + def test_js_or_html + @request.env["HTTP_ACCEPT"] = "text/javascript, text/html" + get :js_or_html + assert_equal 'JS', @response.body + + get :html_or_xml + assert_equal 'HTML', @response.body + + get :just_xml + assert_response 406 + end + + def test_js_or_anything + @request.env["HTTP_ACCEPT"] = "text/javascript, */*" + get :js_or_html + assert_equal 'JS', @response.body + + get :html_or_xml + assert_equal 'HTML', @response.body + + get :just_xml + assert_equal 'XML', @response.body + end + + def test_using_defaults + @request.env["HTTP_ACCEPT"] = "*/*" + get :using_defaults + assert_equal 'Hello world!', @response.body + + @request.env["HTTP_ACCEPT"] = "text/javascript" + get :using_defaults + assert_equal '$("body").visualEffect("highlight");', @response.body + + @request.env["HTTP_ACCEPT"] = "application/xml" + get :using_defaults + assert_equal "

      Hello world!

      \n", @response.body + end + + def test_using_defaults_with_type_list + @request.env["HTTP_ACCEPT"] = "*/*" + get :using_defaults_with_type_list + assert_equal 'Hello world!', @response.body + + @request.env["HTTP_ACCEPT"] = "text/javascript" + get :using_defaults_with_type_list + assert_equal '$("body").visualEffect("highlight");', @response.body + + @request.env["HTTP_ACCEPT"] = "application/xml" + get :using_defaults_with_type_list + assert_equal "

      Hello world!

      \n", @response.body + end + + def test_with_content_type + @request.env["CONTENT_TYPE"] = "application/atom+xml" + get :made_for_content_type + assert_equal "ATOM", @response.body + + @request.env["CONTENT_TYPE"] = "application/rss+xml" + get :made_for_content_type + assert_equal "RSS", @response.body + end + + def test_synonyms + @request.env["HTTP_ACCEPT"] = "application/javascript" + get :js_or_html + assert_equal 'JS', @response.body + + @request.env["HTTP_ACCEPT"] = "application/x-xml" + get :html_xml_or_rss + assert_equal "XML", @response.body + end + + def test_custom_types + @request.env["HTTP_ACCEPT"] = "application/crazy-xml" + get :custom_type_handling + assert_equal 'Crazy XML', @response.body + + @request.env["HTTP_ACCEPT"] = "text/html" + get :custom_type_handling + assert_equal 'HTML', @response.body + end + + def test_xhtml_alias + @request.env["HTTP_ACCEPT"] = "application/xhtml+xml,application/xml" + get :html_or_xml + assert_equal 'HTML', @response.body + end + + def test_firefox_simulation + @request.env["HTTP_ACCEPT"] = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5" + get :html_or_xml + assert_equal 'HTML', @response.body + end + + def test_handle_any + @request.env["HTTP_ACCEPT"] = "*/*" + get :handle_any + assert_equal 'HTML', @response.body + + @request.env["HTTP_ACCEPT"] = "text/javascript" + get :handle_any + assert_equal 'Either JS or XML', @response.body + + @request.env["HTTP_ACCEPT"] = "text/xml" + get :handle_any + assert_equal 'Either JS or XML', @response.body + end + + def test_all_types_with_layout + @request.env["HTTP_ACCEPT"] = "text/javascript" + get :all_types_with_layout + assert_equal 'RJS for all_types_with_layout', @response.body + + @request.env["HTTP_ACCEPT"] = "text/html" + get :all_types_with_layout + assert_equal 'HTML for all_types_with_layout', @response.body + end + + def test_xhr + xhr :get, :js_or_html + assert_equal 'JS', @response.body + + xhr :get, :using_defaults + assert_equal '$("body").visualEffect("highlight");', @response.body + end +end diff --git a/vendor/rails/actionpack/test/controller/mime_type_test.rb b/vendor/rails/actionpack/test/controller/mime_type_test.rb new file mode 100644 index 00000000..aa1d4459 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/mime_type_test.rb @@ -0,0 +1,24 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class MimeTypeTest < Test::Unit::TestCase + Mime::PNG = Mime::Type.new("image/png") + Mime::PLAIN = Mime::Type.new("text/plain") + + def test_parse_single + Mime::LOOKUP.keys.each do |mime_type| + assert_equal [Mime::Type.lookup(mime_type)], Mime::Type.parse(mime_type) + end + end + + def test_parse_without_q + accept = "text/xml,application/xhtml+xml,text/yaml,application/xml,text/html,image/png,text/plain,*/*" + expect = [Mime::HTML, Mime::XML, Mime::YAML, Mime::PNG, Mime::PLAIN, Mime::ALL] + assert_equal expect, Mime::Type.parse(accept) + end + + def test_parse_with_q + accept = "text/xml,application/xhtml+xml,text/yaml; q=0.3,application/xml,text/html; q=0.8,image/png,text/plain; q=0.5,*/*; q=0.2" + expect = [Mime::HTML, Mime::XML, Mime::PNG, Mime::PLAIN, Mime::YAML, Mime::ALL] + assert_equal expect, Mime::Type.parse(accept) + end +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/controller/new_render_test.rb b/vendor/rails/actionpack/test/controller/new_render_test.rb new file mode 100644 index 00000000..0722bb9f --- /dev/null +++ b/vendor/rails/actionpack/test/controller/new_render_test.rb @@ -0,0 +1,600 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +silence_warnings { Customer = Struct.new("Customer", :name) } + +module Fun + class GamesController < ActionController::Base + def hello_world + end + end +end + +module NewRenderTestHelper + def rjs_helper_method_from_module + page.visual_effect :highlight + end +end + +class NewRenderTestController < ActionController::Base + layout :determine_layout + + def self.controller_name; "test"; end + def self.controller_path; "test"; end + + def hello_world + end + + def render_hello_world + render :template => "test/hello_world" + end + + def render_hello_world_from_variable + @person = "david" + render :text => "hello #{@person}" + end + + def render_action_hello_world + render :action => "hello_world" + end + + def render_action_hello_world_as_symbol + render :action => :hello_world + end + + def render_text_hello_world + render :text => "hello world" + end + + def render_text_hello_world_with_layout + @variable_for_layout = ", I'm here!" + render :text => "hello world", :layout => true + end + + def hello_world_with_layout_false + render :layout => false + end + + def render_custom_code + render :text => "hello world", :status => "404 Moved" + end + + def render_file_with_instance_variables + @secret = 'in the sauce' + path = File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar.rhtml') + render :file => path + end + + def render_file_with_locals + path = File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_locals.rhtml') + render :file => path, :locals => {:secret => 'in the sauce'} + end + + def render_file_not_using_full_path + @secret = 'in the sauce' + render :file => 'test/render_file_with_ivar', :use_full_path => true + end + + def render_file_not_using_full_path_with_relative_path + @secret = 'in the sauce' + render :file => 'test/../test/render_file_with_ivar', :use_full_path => true + end + + def render_file_not_using_full_path_with_dot_in_path + @secret = 'in the sauce' + render :file => 'test/dot.directory/render_file_with_ivar', :use_full_path => true + end + + def render_xml_hello + @name = "David" + render :template => "test/hello" + end + + def greeting + # let's just rely on the template + end + + def layout_test + render :action => "hello_world" + end + + def layout_test_with_different_layout + render :action => "hello_world", :layout => "standard" + end + + def rendering_without_layout + render :action => "hello_world", :layout => false + end + + def layout_overriding_layout + render :action => "hello_world", :layout => "standard" + end + + def rendering_nothing_on_layout + render :nothing => true + end + + def builder_layout_test + render :action => "hello" + end + + def partials_list + @test_unchanged = 'hello' + @customers = [ Customer.new("david"), Customer.new("mary") ] + render :action => "list" + end + + def partial_only + render :partial => true + end + + def partial_only_with_layout + render :partial => "partial_only", :layout => true + end + + def partial_with_locals + render :partial => "customer", :locals => { :customer => Customer.new("david") } + end + + def partial_collection + render :partial => "customer", :collection => [ Customer.new("david"), Customer.new("mary") ] + end + + def partial_collection_with_locals + render :partial => "customer_greeting", :collection => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" } + end + + def empty_partial_collection + render :partial => "customer", :collection => [] + end + + def partial_with_hash_object + render :partial => "hash_object", :object => {:first_name => "Sam"} + end + + def partial_with_implicit_local_assignment + @customer = Customer.new("Marcel") + render :partial => "customer" + end + + def hello_in_a_string + @customers = [ Customer.new("david"), Customer.new("mary") ] + render :text => "How's there? #{render_to_string("test/list")}" + end + + def accessing_params_in_template + render :inline => "Hello: <%= params[:name] %>" + end + + def accessing_params_in_template_with_layout + render :layout => nil, :inline => "Hello: <%= params[:name] %>" + end + + def render_with_explicit_template + render "test/hello_world" + end + + def double_render + render :text => "hello" + render :text => "world" + end + + def double_redirect + redirect_to :action => "double_render" + redirect_to :action => "double_render" + end + + def render_and_redirect + render :text => "hello" + redirect_to :action => "double_render" + end + + def rendering_with_conflicting_local_vars + @name = "David" + def @template.name() nil end + render :action => "potential_conflicts" + end + + def hello_world_from_rxml_using_action + render :action => "hello_world.rxml" + end + + def hello_world_from_rxml_using_template + render :template => "test/hello_world.rxml" + end + + helper NewRenderTestHelper + helper do + def rjs_helper_method(value) + page.visual_effect :highlight, value + end + end + + def enum_rjs_test + render :update do |page| + page.select('.product').each do |value| + page.rjs_helper_method_from_module + page.rjs_helper_method(value) + page.sortable(value, :url => { :action => "order" }) + page.draggable(value) + end + end + end + + def delete_with_js + @project_id = 4 + end + + def render_js_with_explicit_template + @project_id = 4 + render :template => 'test/delete_with_js' + end + + def render_js_with_explicit_action_template + @project_id = 4 + render :action => 'delete_with_js' + end + + def update_page + render :update do |page| + page.replace_html 'balance', '$37,000,000.00' + page.visual_effect :highlight, 'balance' + end + end + + def update_page_with_instance_variables + @money = '$37,000,000.00' + @div_id = 'balance' + render :update do |page| + page.replace_html @div_id, @money + page.visual_effect :highlight, @div_id + end + end + + def action_talk_to_layout + # Action template sets variable that's picked up by layout + end + + def render_text_with_assigns + @hello = "world" + render :text => "foo" + end + + def yield_content_for + render :action => "content_for", :layout => "yield" + end + + def rescue_action(e) raise end + + private + def determine_layout + case action_name + when "hello_world", "layout_test", "rendering_without_layout", + "rendering_nothing_on_layout", "render_text_hello_world", + "render_text_hello_world_with_layout", + "hello_world_with_layout_false", + "partial_only", "partial_only_with_layout", + "accessing_params_in_template", + "accessing_params_in_template_with_layout", + "render_with_explicit_template", + "render_js_with_explicit_template", + "render_js_with_explicit_action_template", + "delete_with_js", "update_page", "update_page_with_instance_variables" + + "layouts/standard" + when "builder_layout_test" + "layouts/builder" + when "action_talk_to_layout", "layout_overriding_layout" + "layouts/talk_from_action" + end + end +end + +NewRenderTestController.template_root = File.dirname(__FILE__) + "/../fixtures/" +Fun::GamesController.template_root = File.dirname(__FILE__) + "/../fixtures/" + +class NewRenderTest < Test::Unit::TestCase + def setup + @controller = NewRenderTestController.new + + # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get + # a more accurate simulation of what happens in "real life". + @controller.logger = Logger.new(nil) + + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + + @request.host = "www.nextangle.com" + end + + def test_simple_show + get :hello_world + assert_response :success + assert_template "test/hello_world" + assert_equal "Hello world!", @response.body + end + + def test_do_with_render + get :render_hello_world + assert_template "test/hello_world" + end + + def test_do_with_render_from_variable + get :render_hello_world_from_variable + assert_equal "hello david", @response.body + end + + def test_do_with_render_action + get :render_action_hello_world + assert_template "test/hello_world" + end + + def test_do_with_render_action_as_symbol + get :render_action_hello_world_as_symbol + assert_template "test/hello_world" + end + + def test_do_with_render_text + get :render_text_hello_world + assert_equal "hello world", @response.body + end + + def test_do_with_render_text_and_layout + get :render_text_hello_world_with_layout + assert_equal "hello world, I'm here!", @response.body + end + + def test_do_with_render_action_and_layout_false + get :hello_world_with_layout_false + assert_equal 'Hello world!', @response.body + end + + def test_do_with_render_custom_code + get :render_custom_code + assert_response :missing + end + + def test_render_file_with_instance_variables + get :render_file_with_instance_variables + assert_equal "The secret is in the sauce\n", @response.body + end + + def test_render_file_not_using_full_path + get :render_file_not_using_full_path + assert_equal "The secret is in the sauce\n", @response.body + end + + def test_render_file_not_using_full_path_with_relative_path + get :render_file_not_using_full_path_with_relative_path + assert_equal "The secret is in the sauce\n", @response.body + end + + def test_render_file_not_using_full_path_with_dot_in_path + get :render_file_not_using_full_path_with_dot_in_path + assert_equal "The secret is in the sauce\n", @response.body + end + + def test_render_file_with_locals + get :render_file_with_locals + assert_equal "The secret is in the sauce\n", @response.body + end + + def test_attempt_to_access_object_method + assert_raises(ActionController::UnknownAction, "No action responded to [clone]") { get :clone } + end + + def test_private_methods + assert_raises(ActionController::UnknownAction, "No action responded to [determine_layout]") { get :determine_layout } + end + + def test_access_to_request_in_view + view_internals_old_value = ActionController::Base.view_controller_internals + + ActionController::Base.view_controller_internals = false + ActionController::Base.protected_variables_cache = nil + + get :hello_world + assert_nil(assigns["request"]) + + ActionController::Base.view_controller_internals = true + ActionController::Base.protected_variables_cache = nil + + get :hello_world + assert_kind_of ActionController::AbstractRequest, assigns["request"] + + ActionController::Base.view_controller_internals = view_internals_old_value + ActionController::Base.protected_variables_cache = nil + end + + def test_render_xml + get :render_xml_hello + assert_equal "\n

      Hello David

      \n

      This is grand!

      \n\n", @response.body + end + + def test_enum_rjs_test + get :enum_rjs_test + assert_equal <<-EOS.strip, @response.body +$$(".product").each(function(value, index) { +new Effect.Highlight(element,{}); +new Effect.Highlight(value,{}); +Sortable.create(value, {onUpdate:function(){new Ajax.Request('/test/order', {asynchronous:true, evalScripts:true, parameters:Sortable.serialize(value)})}}); +new Draggable(value, {}); +}); +EOS + end + + def test_render_xml_with_default + get :greeting + assert_equal "

      This is grand!

      \n", @response.body + end + + def test_render_rjs_with_default + get :delete_with_js + assert_equal %!["person"].each(Element.remove);\nnew Effect.Highlight(\"project-4\",{});!, @response.body + end + + def test_render_rjs_template_explicitly + get :render_js_with_explicit_template + assert_equal %!["person"].each(Element.remove);\nnew Effect.Highlight(\"project-4\",{});!, @response.body + end + + def test_rendering_rjs_action_explicitly + get :render_js_with_explicit_action_template + assert_equal %!["person"].each(Element.remove);\nnew Effect.Highlight(\"project-4\",{});!, @response.body + end + + def test_layout_rendering + get :layout_test + assert_equal "Hello world!", @response.body + end + + def test_layout_test_with_different_layout + get :layout_test_with_different_layout + assert_equal "Hello world!", @response.body + end + + def test_rendering_without_layout + get :rendering_without_layout + assert_equal "Hello world!", @response.body + end + + def test_layout_overriding_layout + get :layout_overriding_layout + assert_no_match %r{}, @response.body + end + + def test_rendering_nothing_on_layout + get :rendering_nothing_on_layout + assert_equal " ", @response.body + end + + def test_render_xml_with_layouts + get :builder_layout_test + assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body + end + + def test_partial_only + get :partial_only + assert_equal "only partial", @response.body + end + + def test_partial_only_with_layout + get :partial_only_with_layout + assert_equal "<html>only partial</html>", @response.body + end + + def test_render_to_string + get :hello_in_a_string + assert_equal "How's there? goodbyeHello: davidHello: marygoodbye\n", @response.body + end + + def test_nested_rendering + get :hello_world + assert_equal "Living in a nested world", Fun::GamesController.process(@request, @response).body + end + + def test_accessing_params_in_template + get :accessing_params_in_template, :name => "David" + assert_equal "Hello: David", @response.body + end + + def test_accessing_params_in_template_with_layout + get :accessing_params_in_template_with_layout, :name => "David" + assert_equal "<html>Hello: David</html>", @response.body + end + + def test_render_with_explicit_template + get :render_with_explicit_template + assert_response :success + end + + def test_double_render + assert_raises(ActionController::DoubleRenderError) { get :double_render } + end + + def test_double_redirect + assert_raises(ActionController::DoubleRenderError) { get :double_redirect } + end + + def test_render_and_redirect + assert_raises(ActionController::DoubleRenderError) { get :render_and_redirect } + end + + def test_rendering_with_conflicting_local_vars + get :rendering_with_conflicting_local_vars + assert_equal("First: David\nSecond: Stephan\nThird: David\nFourth: David\nFifth: ", @response.body) + end + + def test_action_talk_to_layout + get :action_talk_to_layout + assert_equal "<title>Talking to the layout\nAction was here!", @response.body + end + + def test_partials_list + get :partials_list + assert_equal "goodbyeHello: davidHello: marygoodbye\n", @response.body + end + + def test_partial_with_locals + get :partial_with_locals + assert_equal "Hello: david", @response.body + end + + def test_partial_collection + get :partial_collection + assert_equal "Hello: davidHello: mary", @response.body + end + + def test_partial_collection_with_locals + get :partial_collection_with_locals + assert_equal "Bonjour: davidBonjour: mary", @response.body + end + + def test_empty_partial_collection + get :empty_partial_collection + assert_equal " ", @response.body + end + + def test_partial_with_hash_object + get :partial_with_hash_object + assert_equal "Sam", @response.body + end + + def test_partial_with_implicit_local_assignment + get :partial_with_implicit_local_assignment + assert_equal "Hello: Marcel", @response.body + end + + def test_render_text_with_assigns + get :render_text_with_assigns + assert_equal "world", assigns["hello"] + end + + def test_update_page + get :update_page + assert_template nil + assert_equal 'text/javascript; charset=UTF-8', @response.headers['Content-Type'] + assert_equal 2, @response.body.split($/).length + end + + def test_update_page_with_instance_variables + get :update_page_with_instance_variables + assert_template nil + assert_equal 'text/javascript; charset=UTF-8', @response.headers['Content-Type'] + assert_match /balance/, @response.body + assert_match /\$37/, @response.body + end + + def test_yield_content_for + get :yield_content_for + assert_equal "Putting stuff in the title!\n\nGreat stuff!\n", @response.body + end + + + def test_overwritting_rendering_relative_file_with_extension + get :hello_world_from_rxml_using_template + assert_equal "\n

      Hello

      \n\n", @response.body + + get :hello_world_from_rxml_using_action + assert_equal "\n

      Hello

      \n\n", @response.body + end +end diff --git a/vendor/rails/actionpack/test/controller/raw_post_test.rb b/vendor/rails/actionpack/test/controller/raw_post_test.rb new file mode 100644 index 00000000..98dc7f6b --- /dev/null +++ b/vendor/rails/actionpack/test/controller/raw_post_test.rb @@ -0,0 +1,31 @@ +require 'test/unit' +require 'cgi' +require 'stringio' +require File.dirname(__FILE__) + '/../../lib/action_controller/cgi_ext/raw_post_data_fix' + +class RawPostDataTest < Test::Unit::TestCase + def setup + ENV['REQUEST_METHOD'] = 'POST' + ENV['CONTENT_TYPE'] = '' + ENV['CONTENT_LENGTH'] = '0' + end + + def test_raw_post_data + process_raw "action=create_customer&full_name=David%20Heinemeier%20Hansson&customerId=1" + end + + private + def process_raw(query_string) + old_stdin = $stdin + begin + $stdin = StringIO.new(query_string.dup) + ENV['CONTENT_LENGTH'] = $stdin.size.to_s + CGI.new + assert_not_nil ENV['RAW_POST_DATA'] + assert ENV['RAW_POST_DATA'].frozen? + assert_equal query_string, ENV['RAW_POST_DATA'] + ensure + $stdin = old_stdin + end + end +end diff --git a/vendor/rails/actionpack/test/controller/redirect_test.rb b/vendor/rails/actionpack/test/controller/redirect_test.rb new file mode 100755 index 00000000..d9b9042d --- /dev/null +++ b/vendor/rails/actionpack/test/controller/redirect_test.rb @@ -0,0 +1,143 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class RedirectController < ActionController::Base + def simple_redirect + redirect_to :action => "hello_world" + end + + def method_redirect + redirect_to :dashbord_url, 1, "hello" + end + + def host_redirect + redirect_to :action => "other_host", :only_path => false, :host => 'other.test.host' + end + + def module_redirect + redirect_to :controller => 'module_test/module_redirect', :action => "hello_world" + end + + def redirect_with_assigns + @hello = "world" + redirect_to :action => "hello_world" + end + + def redirect_to_back + redirect_to :back + end + + def rescue_errors(e) raise e end + + def rescue_action(e) raise end + + protected + def dashbord_url(id, message) + url_for :action => "dashboard", :params => { "id" => id, "message" => message } + end +end + +class RedirectTest < Test::Unit::TestCase + def setup + @controller = RedirectController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_simple_redirect + get :simple_redirect + assert_redirect_url "http://test.host/redirect/hello_world" + end + + def test_redirect_with_method_reference_and_parameters + get :method_redirect + assert_redirect_url "http://test.host/redirect/dashboard/1?message=hello" + end + + def test_simple_redirect_using_options + get :host_redirect + assert_redirected_to :action => "other_host", :only_path => false, :host => 'other.test.host' + end + + def test_redirect_error_with_pretty_diff + get :host_redirect + begin + assert_redirected_to :action => "other_host", :only_path => true + rescue Test::Unit::AssertionFailedError => err + redirection_msg, diff_msg = err.message.scan(/<\{[^\}]+\}>/).collect { |s| s[2..-3] } + assert_match %r(:only_path=>false), redirection_msg + assert_match %r(:host=>"other.test.host"), redirection_msg + assert_match %r(:action=>"other_host"), redirection_msg + assert_match %r(:only_path=>true), diff_msg + assert_match %r(:host=>"other.test.host"), diff_msg + end + end + + def test_module_redirect + get :module_redirect + assert_redirect_url "http://test.host/module_test/module_redirect/hello_world" + end + + def test_module_redirect_using_options + get :module_redirect + assert_redirected_to :controller => 'module_test/module_redirect', :action => 'hello_world' + end + + def test_redirect_with_assigns + get :redirect_with_assigns + assert_equal "world", assigns["hello"] + end + + def test_redirect_to_back + @request.env["HTTP_REFERER"] = "http://www.example.com/coming/from" + get :redirect_to_back + assert_redirect_url "http://www.example.com/coming/from" + end + + def test_redirect_to_back_with_no_referer + assert_raises(ActionController::RedirectBackError) { + @request.env["HTTP_REFERER"] = nil + get :redirect_to_back + } + end +end + +module ModuleTest + class ModuleRedirectController < ::RedirectController + def module_redirect + redirect_to :controller => '/redirect', :action => "hello_world" + end + end + + class ModuleRedirectTest < Test::Unit::TestCase + def setup + @controller = ModuleRedirectController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_simple_redirect + get :simple_redirect + assert_redirect_url "http://test.host/module_test/module_redirect/hello_world" + end + + def test_redirect_with_method_reference_and_parameters + get :method_redirect + assert_redirect_url "http://test.host/module_test/module_redirect/dashboard/1?message=hello" + end + + def test_simple_redirect_using_options + get :host_redirect + assert_redirected_to :action => "other_host", :only_path => false, :host => 'other.test.host' + end + + def test_module_redirect + get :module_redirect + assert_redirect_url "http://test.host/redirect/hello_world" + end + + def test_module_redirect_using_options + get :module_redirect + assert_redirected_to :controller => 'redirect', :action => "hello_world" + end + end +end diff --git a/vendor/rails/actionpack/test/controller/render_test.rb b/vendor/rails/actionpack/test/controller/render_test.rb new file mode 100644 index 00000000..1ee77a54 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/render_test.rb @@ -0,0 +1,246 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +unless defined?(Customer) + Customer = Struct.new("Customer", :name) +end + +module Fun + class GamesController < ActionController::Base + def hello_world + end + end +end + + +class TestController < ActionController::Base + layout :determine_layout + + def hello_world + end + + def render_hello_world + render "test/hello_world" + end + + def render_hello_world_from_variable + @person = "david" + render_text "hello #{@person}" + end + + def render_action_hello_world + render_action "hello_world" + end + + def render_action_hello_world_with_symbol + render_action :hello_world + end + + def render_text_hello_world + render_text "hello world" + end + + def render_custom_code + render_text "hello world", "404 Moved" + end + + def render_xml_hello + @name = "David" + render "test/hello" + end + + def greeting + # let's just rely on the template + end + + def layout_test + render_action "hello_world" + end + + def builder_layout_test + render_action "hello" + end + + def partials_list + @test_unchanged = 'hello' + @customers = [ Customer.new("david"), Customer.new("mary") ] + render_action "list" + end + + def partial_only + render_partial + end + + def hello_in_a_string + @customers = [ Customer.new("david"), Customer.new("mary") ] + render_text "How's there? #{render_to_string("test/list")}" + end + + def accessing_params_in_template + render_template "Hello: <%= params[:name] %>" + end + + def accessing_local_assigns_in_inline_template + name = params[:local_name] + render :inline => "<%= 'Goodbye, ' + local_name %>", + :locals => { :local_name => name } + end + + def accessing_local_assigns_in_inline_template_with_string_keys + name = params[:local_name] + ActionView::Base.local_assigns_support_string_keys = true + render :inline => "<%= 'Goodbye, ' + local_name %>", + :locals => { "local_name" => name } + ActionView::Base.local_assigns_support_string_keys = false + end + + def render_to_string_test + @foo = render_to_string :inline => "this is a test" + end + + def rescue_action(e) raise end + + private + def determine_layout + case action_name + when "layout_test": "layouts/standard" + when "builder_layout_test": "layouts/builder" + end + end +end + +TestController.template_root = File.dirname(__FILE__) + "/../fixtures/" +Fun::GamesController.template_root = File.dirname(__FILE__) + "/../fixtures/" + +class RenderTest < Test::Unit::TestCase + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @controller = TestController.new + + @request.host = "www.nextangle.com" + end + + def test_simple_show + get :hello_world + assert_response 200 + assert_template "test/hello_world" + end + + def test_do_with_render + get :render_hello_world + assert_template "test/hello_world" + end + + def test_do_with_render_from_variable + get :render_hello_world_from_variable + assert_equal "hello david", @response.body + end + + def test_do_with_render_action + get :render_action_hello_world + assert_template "test/hello_world" + end + + def test_do_with_render_action_with_symbol + get :render_action_hello_world_with_symbol + assert_template "test/hello_world" + end + + def test_do_with_render_text + get :render_text_hello_world + assert_equal "hello world", @response.body + end + + def test_do_with_render_custom_code + get :render_custom_code + assert_response 404 + end + + def test_attempt_to_access_object_method + assert_raises(ActionController::UnknownAction, "No action responded to [clone]") { get :clone } + end + + def test_private_methods + assert_raises(ActionController::UnknownAction, "No action responded to [determine_layout]") { get :determine_layout } + end + + def test_access_to_request_in_view + view_internals_old_value = ActionController::Base.view_controller_internals + + ActionController::Base.view_controller_internals = false + ActionController::Base.protected_variables_cache = nil + + get :hello_world + assert_nil assigns["request"] + + ActionController::Base.view_controller_internals = true + ActionController::Base.protected_variables_cache = nil + + get :hello_world + assert_kind_of ActionController::AbstractRequest, assigns["request"] + + ActionController::Base.view_controller_internals = view_internals_old_value + ActionController::Base.protected_variables_cache = nil + end + + def test_render_xml + get :render_xml_hello + assert_equal "\n

      Hello David

      \n

      This is grand!

      \n\n", @response.body + end + + def test_render_xml_with_default + get :greeting + assert_equal "

      This is grand!

      \n", @response.body + end + + def test_layout_rendering + get :layout_test + assert_equal "Hello world!", @response.body + end + + def test_render_xml_with_layouts + get :builder_layout_test + assert_equal "\n\n

      Hello

      \n

      This is grand!

      \n\n
      \n", @response.body + end + + # def test_partials_list + # get :partials_list + # assert_equal "goodbyeHello: davidHello: marygoodbye\n", process_request.body + # end + + def test_partial_only + get :partial_only + assert_equal "only partial", @response.body + end + + def test_render_to_string + get :hello_in_a_string + assert_equal "How's there? goodbyeHello: davidHello: marygoodbye\n", @response.body + end + + def test_render_to_string_resets_assigns + get :render_to_string_test + assert_equal "The value of foo is: ::this is a test::\n", @response.body + end + + def test_nested_rendering + @controller = Fun::GamesController.new + get :hello_world + assert_equal "Living in a nested world", @response.body + end + + def test_accessing_params_in_template + get :accessing_params_in_template, :name => "David" + assert_equal "Hello: David", @response.body + end + + def test_accessing_local_assigns_in_inline_template + get :accessing_local_assigns_in_inline_template, :local_name => "Local David" + assert_equal "Goodbye, Local David", @response.body + end + + def test_accessing_local_assigns_in_inline_template_with_string_keys + get :accessing_local_assigns_in_inline_template_with_string_keys, :local_name => "Local David" + assert_equal "Goodbye, Local David", @response.body + end +end diff --git a/vendor/rails/actionpack/test/controller/request_test.rb b/vendor/rails/actionpack/test/controller/request_test.rb new file mode 100644 index 00000000..43cd8836 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/request_test.rb @@ -0,0 +1,266 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class RequestTest < Test::Unit::TestCase + def setup + @request = ActionController::TestRequest.new + end + + def test_remote_ip + assert_equal '0.0.0.0', @request.remote_ip + + @request.remote_addr = '1.2.3.4' + assert_equal '1.2.3.4', @request.remote_ip + + @request.env['HTTP_CLIENT_IP'] = '2.3.4.5' + assert_equal '2.3.4.5', @request.remote_ip + @request.env.delete 'HTTP_CLIENT_IP' + + @request.env['HTTP_X_FORWARDED_FOR'] = '3.4.5.6' + assert_equal '3.4.5.6', @request.remote_ip + + @request.env['HTTP_X_FORWARDED_FOR'] = 'unknown,3.4.5.6' + assert_equal '3.4.5.6', @request.remote_ip + + @request.env['HTTP_X_FORWARDED_FOR'] = '172.16.0.1,3.4.5.6' + assert_equal '3.4.5.6', @request.remote_ip + + @request.env['HTTP_X_FORWARDED_FOR'] = '192.168.0.1,3.4.5.6' + assert_equal '3.4.5.6', @request.remote_ip + + @request.env['HTTP_X_FORWARDED_FOR'] = '10.0.0.1,3.4.5.6' + assert_equal '3.4.5.6', @request.remote_ip + + @request.env['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,3.4.5.6' + assert_equal '127.0.0.1', @request.remote_ip + + @request.env['HTTP_X_FORWARDED_FOR'] = 'unknown,192.168.0.1' + assert_equal '1.2.3.4', @request.remote_ip + @request.env.delete 'HTTP_X_FORWARDED_FOR' + end + + def test_domains + @request.host = "www.rubyonrails.org" + assert_equal "rubyonrails.org", @request.domain + + @request.host = "www.rubyonrails.co.uk" + assert_equal "rubyonrails.co.uk", @request.domain(2) + + @request.host = "192.168.1.200" + assert_nil @request.domain + + @request.host = nil + assert_nil @request.domain + end + + def test_subdomains + @request.host = "www.rubyonrails.org" + assert_equal %w( www ), @request.subdomains + + @request.host = "www.rubyonrails.co.uk" + assert_equal %w( www ), @request.subdomains(2) + + @request.host = "dev.www.rubyonrails.co.uk" + assert_equal %w( dev www ), @request.subdomains(2) + + @request.host = "foobar.foobar.com" + assert_equal %w( foobar ), @request.subdomains + + @request.host = nil + assert_equal [], @request.subdomains + end + + def test_port_string + @request.port = 80 + assert_equal "", @request.port_string + + @request.port = 8080 + assert_equal ":8080", @request.port_string + end + + def test_relative_url_root + @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" + @request.env['SERVER_SOFTWARE'] = 'lighttpd/1.2.3' + assert_equal '', @request.relative_url_root, "relative_url_root should be disabled on lighttpd" + + @request.env['SERVER_SOFTWARE'] = 'apache/1.2.3 some random text' + + @request.env['SCRIPT_NAME'] = nil + assert_equal "", @request.relative_url_root + + @request.env['SCRIPT_NAME'] = "/dispatch.cgi" + assert_equal "", @request.relative_url_root + + @request.env['SCRIPT_NAME'] = "/myapp.rb" + assert_equal "", @request.relative_url_root + + @request.relative_url_root = nil + @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" + assert_equal "/hieraki", @request.relative_url_root + + @request.relative_url_root = nil + @request.env['SCRIPT_NAME'] = "/collaboration/hieraki/dispatch.cgi" + assert_equal "/collaboration/hieraki", @request.relative_url_root + + # apache/scgi case + @request.relative_url_root = nil + @request.env['SCRIPT_NAME'] = "/collaboration/hieraki" + assert_equal "/collaboration/hieraki", @request.relative_url_root + + @request.relative_url_root = nil + @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" + @request.env['SERVER_SOFTWARE'] = 'lighttpd/1.2.3' + @request.env['RAILS_RELATIVE_URL_ROOT'] = "/hieraki" + assert_equal "/hieraki", @request.relative_url_root + + # @env overrides path guess + @request.relative_url_root = nil + @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" + @request.env['SERVER_SOFTWARE'] = 'apache/1.2.3 some random text' + @request.env['RAILS_RELATIVE_URL_ROOT'] = "/real_url" + assert_equal "/real_url", @request.relative_url_root + end + + def test_request_uri + @request.env['SERVER_SOFTWARE'] = 'Apache 42.342.3432' + + @request.relative_url_root = nil + @request.set_REQUEST_URI "http://www.rubyonrails.org/path/of/some/uri?mapped=1" + assert_equal "/path/of/some/uri?mapped=1", @request.request_uri + assert_equal "/path/of/some/uri", @request.path + + @request.relative_url_root = nil + @request.set_REQUEST_URI "http://www.rubyonrails.org/path/of/some/uri" + assert_equal "/path/of/some/uri", @request.request_uri + assert_equal "/path/of/some/uri", @request.path + + @request.relative_url_root = nil + @request.set_REQUEST_URI "/path/of/some/uri" + assert_equal "/path/of/some/uri", @request.request_uri + assert_equal "/path/of/some/uri", @request.path + + @request.relative_url_root = nil + @request.set_REQUEST_URI "/" + assert_equal "/", @request.request_uri + assert_equal "/", @request.path + + @request.relative_url_root = nil + @request.set_REQUEST_URI "/?m=b" + assert_equal "/?m=b", @request.request_uri + assert_equal "/", @request.path + + @request.relative_url_root = nil + @request.set_REQUEST_URI "/" + @request.env['SCRIPT_NAME'] = "/dispatch.cgi" + assert_equal "/", @request.request_uri + assert_equal "/", @request.path + + @request.relative_url_root = nil + @request.set_REQUEST_URI "/hieraki/" + @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" + assert_equal "/hieraki/", @request.request_uri + assert_equal "/", @request.path + + @request.relative_url_root = nil + @request.set_REQUEST_URI "/collaboration/hieraki/books/edit/2" + @request.env['SCRIPT_NAME'] = "/collaboration/hieraki/dispatch.cgi" + assert_equal "/collaboration/hieraki/books/edit/2", @request.request_uri + assert_equal "/books/edit/2", @request.path + + # The following tests are for when REQUEST_URI is not supplied (as in IIS) + @request.relative_url_root = nil + @request.set_REQUEST_URI nil + @request.env['PATH_INFO'] = "/path/of/some/uri?mapped=1" + @request.env['SCRIPT_NAME'] = nil #"/path/dispatch.rb" + assert_equal "/path/of/some/uri?mapped=1", @request.request_uri + assert_equal "/path/of/some/uri", @request.path + + @request.relative_url_root = nil + @request.env['PATH_INFO'] = "/path/of/some/uri?mapped=1" + @request.env['SCRIPT_NAME'] = "/path/dispatch.rb" + assert_equal "/path/of/some/uri?mapped=1", @request.request_uri + assert_equal "/of/some/uri", @request.path + + @request.relative_url_root = nil + @request.env['PATH_INFO'] = "/path/of/some/uri" + @request.env['SCRIPT_NAME'] = nil + assert_equal "/path/of/some/uri", @request.request_uri + assert_equal "/path/of/some/uri", @request.path + + @request.relative_url_root = nil + @request.env['PATH_INFO'] = "/" + assert_equal "/", @request.request_uri + assert_equal "/", @request.path + + @request.relative_url_root = nil + @request.env['PATH_INFO'] = "/?m=b" + assert_equal "/?m=b", @request.request_uri + assert_equal "/", @request.path + + @request.relative_url_root = nil + @request.env['PATH_INFO'] = "/" + @request.env['SCRIPT_NAME'] = "/dispatch.cgi" + assert_equal "/", @request.request_uri + assert_equal "/", @request.path + + @request.relative_url_root = nil + @request.env['PATH_INFO'] = "/hieraki/" + @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" + assert_equal "/hieraki/", @request.request_uri + assert_equal "/", @request.path + + # This test ensures that Rails uses REQUEST_URI over PATH_INFO + @request.relative_url_root = nil + @request.env['REQUEST_URI'] = "/some/path" + @request.env['PATH_INFO'] = "/another/path" + @request.env['SCRIPT_NAME'] = "/dispatch.cgi" + assert_equal "/some/path", @request.request_uri + assert_equal "/some/path", @request.path + end + + + def test_host_with_port + @request.host = "rubyonrails.org" + @request.port = 80 + assert_equal "rubyonrails.org", @request.host_with_port + + @request.host = "rubyonrails.org" + @request.port = 81 + assert_equal "rubyonrails.org:81", @request.host_with_port + end + + def test_server_software + assert_equal nil, @request.server_software + + @request.env['SERVER_SOFTWARE'] = 'Apache3.422' + assert_equal 'apache', @request.server_software + + @request.env['SERVER_SOFTWARE'] = 'lighttpd(1.1.4)' + assert_equal 'lighttpd', @request.server_software + end + + def test_xml_http_request + assert !@request.xml_http_request? + assert !@request.xhr? + + @request.env['HTTP_X_REQUESTED_WITH'] = "DefinitelyNotAjax1.0" + assert !@request.xml_http_request? + assert !@request.xhr? + + @request.env['HTTP_X_REQUESTED_WITH'] = "XMLHttpRequest" + assert @request.xml_http_request? + assert @request.xhr? + end + + def test_reports_ssl + assert !@request.ssl? + @request.env['HTTPS'] = 'on' + assert @request.ssl? + end + + def test_reports_ssl_when_proxied_via_lighttpd + assert !@request.ssl? + @request.env['HTTP_X_FORWARDED_PROTO'] = 'https' + assert @request.ssl? + end + +end diff --git a/vendor/rails/actionpack/test/controller/routing_test.rb b/vendor/rails/actionpack/test/controller/routing_test.rb new file mode 100644 index 00000000..035b1b55 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/routing_test.rb @@ -0,0 +1,1049 @@ +require File.dirname(__FILE__) + '/../abstract_unit' +require 'test/unit' +require File.dirname(__FILE__) + '/fake_controllers' +require 'stringio' + +RunTimeTests = ARGV.include? 'time' + +module ActionController::CodeGeneration + +class SourceTests < Test::Unit::TestCase + attr_accessor :source + def setup + @source = Source.new + end + + def test_initial_state + assert_equal [], source.lines + assert_equal 0, source.indentation_level + end + + def test_trivial_operations + source << "puts 'Hello World'" + assert_equal ["puts 'Hello World'"], source.lines + assert_equal "puts 'Hello World'", source.to_s + + source.line "puts 'Goodbye World'" + assert_equal ["puts 'Hello World'", "puts 'Goodbye World'"], source.lines + assert_equal "puts 'Hello World'\nputs 'Goodbye World'", source.to_s + end + + def test_indentation + source << "x = gets.to_i" + source << 'if x.odd?' + source.indent { source << "puts 'x is odd!'" } + source << 'else' + source.indent { source << "puts 'x is even!'" } + source << 'end' + + assert_equal ["x = gets.to_i", "if x.odd?", " puts 'x is odd!'", 'else', " puts 'x is even!'", 'end'], source.lines + + text = "x = gets.to_i +if x.odd? + puts 'x is odd!' +else + puts 'x is even!' +end" + + assert_equal text, source.to_s + end +end + +class CodeGeneratorTests < Test::Unit::TestCase + attr_accessor :generator + def setup + @generator = CodeGenerator.new + end + + def test_initial_state + assert_equal [], generator.source.lines + assert_equal [], generator.locals + end + + def test_trivial_operations + ["puts 'Hello World'", "puts 'Goodbye World'"].each {|l| generator << l} + assert_equal ["puts 'Hello World'", "puts 'Goodbye World'"], generator.source.lines + assert_equal "puts 'Hello World'\nputs 'Goodbye World'", generator.to_s + end + + def test_if + generator << "x = gets.to_i" + generator.if("x.odd?") { generator << "puts 'x is odd!'" } + + assert_equal "x = gets.to_i\nif x.odd?\n puts 'x is odd!'\nend", generator.to_s + end + + def test_else + test_if + generator.else { generator << "puts 'x is even!'" } + + assert_equal "x = gets.to_i\nif x.odd?\n puts 'x is odd!'\nelse \n puts 'x is even!'\nend", generator.to_s + end + + def test_dup + generator << 'x = 2' + generator.locals << :x + + g = generator.dup + assert_equal generator.source, g.source + assert_equal generator.locals, g.locals + + g << 'y = 3' + g.locals << :y + assert_equal [:x, :y], g.locals # Make sure they don't share the same array. + assert_equal [:x], generator.locals + end +end + +class RecognitionTests < Test::Unit::TestCase + attr_accessor :generator + alias :g :generator + def setup + @generator = RecognitionGenerator.new + end + + def go(components) + g.current = components.first + g.after = components[1..-1] || [] + g.go + end + + def execute(path, show = false) + path = path.split('/') if path.is_a? String + source = "index, path = 0, #{path.inspect}\n#{g.to_s}" + puts source if show + r = eval source + r ? r.symbolize_keys : nil + end + + Static = ::ActionController::Routing::StaticComponent + Dynamic = ::ActionController::Routing::DynamicComponent + Path = ::ActionController::Routing::PathComponent + Controller = ::ActionController::Routing::ControllerComponent + + def test_all_static + c = %w(hello world how are you).collect {|str| Static.new(str)} + + g.result :controller, "::ContentController", true + g.constant_result :action, 'index' + + go c + + assert_nil execute('x') + assert_nil execute('hello/world/how') + assert_nil execute('hello/world/how/are') + assert_nil execute('hello/world/how/are/you/today') + assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hello/world/how/are/you')) + end + + def test_basic_dynamic + c = [Static.new("hi"), Dynamic.new(:action)] + g.result :controller, "::ContentController", true + go c + + assert_nil execute('boo') + assert_nil execute('boo/blah') + assert_nil execute('hi') + assert_nil execute('hi/dude/what') + assert_equal({:controller => ::ContentController, :action => 'dude'}, execute('hi/dude')) + end + + def test_basic_dynamic_backwards + c = [Dynamic.new(:action), Static.new("hi")] + go c + + assert_nil execute('') + assert_nil execute('boo') + assert_nil execute('boo/blah') + assert_nil execute('hi') + assert_equal({:action => 'index'}, execute('index/hi')) + assert_equal({:action => 'show'}, execute('show/hi')) + assert_nil execute('hi/dude') + end + + def test_dynamic_with_default + c = [Static.new("hi"), Dynamic.new(:action, :default => 'index')] + g.result :controller, "::ContentController", true + go c + + assert_nil execute('boo') + assert_nil execute('boo/blah') + assert_nil execute('hi/dude/what') + assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hi')) + assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hi/index')) + assert_equal({:controller => ::ContentController, :action => 'dude'}, execute('hi/dude')) + end + + def test_dynamic_with_string_condition + c = [Static.new("hi"), Dynamic.new(:action, :condition => 'index')] + g.result :controller, "::ContentController", true + go c + + assert_nil execute('boo') + assert_nil execute('boo/blah') + assert_nil execute('hi') + assert_nil execute('hi/dude/what') + assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hi/index')) + assert_nil execute('hi/dude') + end + + def test_dynamic_with_string_condition_backwards + c = [Dynamic.new(:action, :condition => 'index'), Static.new("hi")] + g.result :controller, "::ContentController", true + go c + + assert_nil execute('boo') + assert_nil execute('boo/blah') + assert_nil execute('hi') + assert_nil execute('dude/what/hi') + assert_nil execute('index/what') + assert_equal({:controller => ::ContentController, :action => 'index'}, execute('index/hi')) + assert_nil execute('dude/hi') + end + + def test_dynamic_with_regexp_condition + c = [Static.new("hi"), Dynamic.new(:action, :condition => /^[a-z]+$/)] + g.result :controller, "::ContentController", true + go c + + assert_nil execute('boo') + assert_nil execute('boo/blah') + assert_nil execute('hi') + assert_nil execute('hi/FOXY') + assert_nil execute('hi/138708jkhdf') + assert_nil execute('hi/dkjfl8792343dfsf') + assert_nil execute('hi/dude/what') + assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hi/index')) + assert_equal({:controller => ::ContentController, :action => 'dude'}, execute('hi/dude')) + end + + def test_dynamic_with_regexp_and_default + c = [Static.new("hi"), Dynamic.new(:action, :condition => /^[a-z]+$/, :default => 'index')] + g.result :controller, "::ContentController", true + go c + + assert_nil execute('boo') + assert_nil execute('boo/blah') + assert_nil execute('hi/FOXY') + assert_nil execute('hi/138708jkhdf') + assert_nil execute('hi/dkjfl8792343dfsf') + assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hi')) + assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hi/index')) + assert_equal({:controller => ::ContentController, :action => 'dude'}, execute('hi/dude')) + assert_nil execute('hi/dude/what') + end + + def test_path + c = [Static.new("hi"), Path.new(:file)] + g.result :controller, "::ContentController", true + g.constant_result :action, "download" + + go c + + assert_nil execute('boo') + assert_nil execute('boo/blah') + assert_equal({:controller => ::ContentController, :action => 'download', :file => []}, execute('hi')) + assert_equal({:controller => ::ContentController, :action => 'download', :file => %w(books agile_rails_dev.pdf)}, + execute('hi/books/agile_rails_dev.pdf')) + assert_equal({:controller => ::ContentController, :action => 'download', :file => ['dude']}, execute('hi/dude')) + assert_equal 'dude/what', execute('hi/dude/what')[:file].to_s + end + + def test_path_with_dynamic + c = [Dynamic.new(:action), Path.new(:file)] + g.result :controller, "::ContentController", true + + go c + + assert_nil execute('') + assert_equal({:controller => ::ContentController, :action => 'download', :file => []}, execute('download')) + assert_equal({:controller => ::ContentController, :action => 'download', :file => %w(books agile_rails_dev.pdf)}, + execute('download/books/agile_rails_dev.pdf')) + assert_equal({:controller => ::ContentController, :action => 'download', :file => ['dude']}, execute('download/dude')) + assert_equal 'dude/what', execute('hi/dude/what')[:file].to_s + end + + def test_path_with_dynamic_and_default + c = [Dynamic.new(:action, :default => 'index'), Path.new(:file)] + + go c + + assert_equal({:action => 'index', :file => []}, execute('')) + assert_equal({:action => 'index', :file => []}, execute('index')) + assert_equal({:action => 'blarg', :file => []}, execute('blarg')) + assert_equal({:action => 'index', :file => ['content']}, execute('index/content')) + assert_equal({:action => 'show', :file => ['rails_dev.pdf']}, execute('show/rails_dev.pdf')) + end + + def test_controller + c = [Static.new("hi"), Controller.new(:controller)] + g.constant_result :action, "hi" + + go c + + assert_nil execute('boo') + assert_nil execute('boo/blah') + assert_nil execute('hi/x') + assert_nil execute('hi/13870948') + assert_nil execute('hi/content/dog') + assert_nil execute('hi/admin/user/foo') + assert_equal({:controller => ::ContentController, :action => 'hi'}, execute('hi/content')) + assert_equal({:controller => ::Admin::UserController, :action => 'hi'}, execute('hi/admin/user')) + end + + def test_controller_with_regexp + c = [Static.new("hi"), Controller.new(:controller, :condition => /^admin\/.+$/)] + g.constant_result :action, "hi" + + go c + + assert_nil execute('hi') + assert_nil execute('hi/x') + assert_nil execute('hi/content') + assert_equal({:controller => ::Admin::UserController, :action => 'hi'}, execute('hi/admin/user')) + assert_equal({:controller => ::Admin::NewsFeedController, :action => 'hi'}, execute('hi/admin/news_feed')) + assert_nil execute('hi/admin/user/foo') + end + + def test_standard_route(time = ::RunTimeTests) + c = [Controller.new(:controller), Dynamic.new(:action, :default => 'index'), Dynamic.new(:id, :default => nil)] + go c + + # Make sure we get the right answers + assert_equal({:controller => ::ContentController, :action => 'index'}, execute('content')) + assert_equal({:controller => ::ContentController, :action => 'list'}, execute('content/list')) + assert_equal({:controller => ::ContentController, :action => 'show', :id => '10'}, execute('content/show/10')) + + assert_equal({:controller => ::Admin::UserController, :action => 'index'}, execute('admin/user')) + assert_equal({:controller => ::Admin::UserController, :action => 'list'}, execute('admin/user/list')) + assert_equal({:controller => ::Admin::UserController, :action => 'show', :id => 'nseckar'}, execute('admin/user/show/nseckar')) + + assert_nil execute('content/show/10/20') + assert_nil execute('food') + + if time + source = "def self.execute(path) + path = path.split('/') if path.is_a? String + index = 0 + r = #{g.to_s} + end" + eval(source) + + GC.start + n = 1000 + time = Benchmark.realtime do n.times { + execute('content') + execute('content/list') + execute('content/show/10') + + execute('admin/user') + execute('admin/user/list') + execute('admin/user/show/nseckar') + + execute('admin/user/show/nseckar/dude') + execute('admin/why/show/nseckar') + execute('content/show/10/20') + execute('food') + } end + time -= Benchmark.realtime do n.times { } end + + + puts "\n\nRecognition:" + per_url = time / (n * 10) + + puts "#{per_url * 1000} ms/url" + puts "#{1 / per_url} urls/s\n\n" + end + end + + def test_default_route + g.result :controller, "::ContentController", true + g.constant_result :action, 'index' + + go [] + + assert_nil execute('x') + assert_nil execute('hello/world/how') + assert_nil execute('hello/world/how/are') + assert_nil execute('hello/world/how/are/you/today') + assert_equal({:controller => ::ContentController, :action => 'index'}, execute([])) + end +end + +class GenerationTests < Test::Unit::TestCase + attr_accessor :generator + alias :g :generator + def setup + @generator = GenerationGenerator.new # ha! + end + + def go(components) + g.current = components.first + g.after = components[1..-1] || [] + g.go + end + + def execute(options, recall, show = false) + source = "\n +expire_on = ::ActionController::Routing.expiry_hash(options, recall) +hash = merged = recall.merge(options) +not_expired = true + +#{g.to_s}\n\n" + puts source if show + eval(source) + end + + Static = ::ActionController::Routing::StaticComponent + Dynamic = ::ActionController::Routing::DynamicComponent + Path = ::ActionController::Routing::PathComponent + Controller = ::ActionController::Routing::ControllerComponent + + def test_all_static_no_requirements + c = [Static.new("hello"), Static.new("world")] + go c + + assert_equal "/hello/world", execute({}, {}) + end + + def test_basic_dynamic + c = [Static.new("hi"), Dynamic.new(:action)] + go c + + assert_equal '/hi/index', execute({:action => 'index'}, {:action => 'index'}) + assert_equal '/hi/show', execute({:action => 'show'}, {:action => 'index'}) + assert_equal '/hi/list+people', execute({}, {:action => 'list people'}) + assert_nil execute({},{}) + end + + def test_dynamic_with_default + c = [Static.new("hi"), Dynamic.new(:action, :default => 'index')] + go c + + assert_equal '/hi', execute({:action => 'index'}, {:action => 'index'}) + assert_equal '/hi/show', execute({:action => 'show'}, {:action => 'index'}) + assert_equal '/hi/list+people', execute({}, {:action => 'list people'}) + assert_equal '/hi', execute({}, {}) + end + + def test_dynamic_with_regexp_condition + c = [Static.new("hi"), Dynamic.new(:action, :condition => /^[a-z]+$/)] + go c + + assert_equal '/hi/index', execute({:action => 'index'}, {:action => 'index'}) + assert_nil execute({:action => 'fox5'}, {:action => 'index'}) + assert_nil execute({:action => 'something_is_up'}, {:action => 'index'}) + assert_nil execute({}, {:action => 'list people'}) + assert_equal '/hi/abunchofcharacter', execute({:action => 'abunchofcharacter'}, {}) + assert_nil execute({}, {}) + end + + def test_dynamic_with_default_and_regexp_condition + c = [Static.new("hi"), Dynamic.new(:action, :default => 'index', :condition => /^[a-z]+$/)] + go c + + assert_equal '/hi', execute({:action => 'index'}, {:action => 'index'}) + assert_nil execute({:action => 'fox5'}, {:action => 'index'}) + assert_nil execute({:action => 'something_is_up'}, {:action => 'index'}) + assert_nil execute({}, {:action => 'list people'}) + assert_equal '/hi/abunchofcharacter', execute({:action => 'abunchofcharacter'}, {}) + assert_equal '/hi', execute({}, {}) + end + + def test_path + c = [Static.new("hi"), Path.new(:file)] + go c + + assert_equal '/hi', execute({:file => []}, {}) + assert_equal '/hi/books/agile_rails_dev.pdf', execute({:file => %w(books agile_rails_dev.pdf)}, {}) + assert_equal '/hi/books/development%26whatever/agile_rails_dev.pdf', execute({:file => %w(books development&whatever agile_rails_dev.pdf)}, {}) + + assert_equal '/hi', execute({:file => ''}, {}) + assert_equal '/hi/books/agile_rails_dev.pdf', execute({:file => 'books/agile_rails_dev.pdf'}, {}) + assert_equal '/hi/books/development%26whatever/agile_rails_dev.pdf', execute({:file => 'books/development&whatever/agile_rails_dev.pdf'}, {}) + end + + def test_controller + c = [Static.new("hi"), Controller.new(:controller)] + go c + + assert_nil execute({}, {}) + assert_equal '/hi/content', execute({:controller => 'content'}, {}) + assert_equal '/hi/admin/user', execute({:controller => 'admin/user'}, {}) + assert_equal '/hi/content', execute({}, {:controller => 'content'}) + assert_equal '/hi/admin/user', execute({}, {:controller => 'admin/user'}) + end + + def test_controller_with_regexp + c = [Static.new("hi"), Controller.new(:controller, :condition => /^admin\/.+$/)] + go c + + assert_nil execute({}, {}) + assert_nil execute({:controller => 'content'}, {}) + assert_equal '/hi/admin/user', execute({:controller => 'admin/user'}, {}) + assert_nil execute({}, {:controller => 'content'}) + assert_equal '/hi/admin/user', execute({}, {:controller => 'admin/user'}) + end + + def test_standard_route(time = ::RunTimeTests) + c = [Controller.new(:controller), Dynamic.new(:action, :default => 'index'), Dynamic.new(:id, :default => nil)] + go c + + # Make sure we get the right answers + assert_equal('/content', execute({:action => 'index'}, {:controller => 'content', :action => 'list'})) + assert_equal('/content/list', execute({:action => 'list'}, {:controller => 'content', :action => 'index'})) + assert_equal('/content/show/10', execute({:action => 'show', :id => '10'}, {:controller => 'content', :action => 'list'})) + + assert_equal('/admin/user', execute({:action => 'index'}, {:controller => 'admin/user', :action => 'list'})) + assert_equal('/admin/user/list', execute({:action => 'list'}, {:controller => 'admin/user', :action => 'index'})) + assert_equal('/admin/user/show/10', execute({:action => 'show', :id => '10'}, {:controller => 'admin/user', :action => 'list'})) + + if time + GC.start + n = 1000 + time = Benchmark.realtime do n.times { + execute({:action => 'index'}, {:controller => 'content', :action => 'list'}) + execute({:action => 'list'}, {:controller => 'content', :action => 'index'}) + execute({:action => 'show', :id => '10'}, {:controller => 'content', :action => 'list'}) + + execute({:action => 'index'}, {:controller => 'admin/user', :action => 'list'}) + execute({:action => 'list'}, {:controller => 'admin/user', :action => 'index'}) + execute({:action => 'show', :id => '10'}, {:controller => 'admin/user', :action => 'list'}) + } end + time -= Benchmark.realtime do n.times { } end + + puts "\n\nGeneration:" + per_url = time / (n * 6) + + puts "#{per_url * 1000} ms/url" + puts "#{1 / per_url} urls/s\n\n" + end + end + + def test_default_route + g.if(g.check_conditions(:controller => 'content', :action => 'welcome')) { go [] } + + assert_nil execute({:controller => 'foo', :action => 'welcome'}, {}) + assert_nil execute({:controller => 'content', :action => 'elcome'}, {}) + assert_nil execute({:action => 'elcome'}, {:controller => 'content'}) + + assert_equal '/', execute({:controller => 'content', :action => 'welcome'}, {}) + assert_equal '/', execute({:action => 'welcome'}, {:controller => 'content'}) + assert_equal '/', execute({:action => 'welcome', :id => '10'}, {:controller => 'content'}) + end +end + +class RouteTests < Test::Unit::TestCase + + def route(*args) + @route = ::ActionController::Routing::Route.new(*args) unless args.empty? + return @route + end + + def rec(path, show = false) + path = path.split('/') if path.is_a? String + index = 0 + source = route.write_recognition.to_s + puts "\n\n#{source}\n\n" if show + r = eval(source) + r ? r.symbolize_keys : r + end + def gen(options, recall = nil, show = false) + recall ||= options.dup + + expire_on = ::ActionController::Routing.expiry_hash(options, recall) + hash = merged = recall.merge(options) + not_expired = true + + source = route.write_generation.to_s + puts "\n\n#{source}\n\n" if show + eval(source) + + end + + def test_static + route 'hello/world', :known => 'known_value', :controller => 'content', :action => 'index' + + assert_nil rec('hello/turn') + assert_nil rec('turn/world') + assert_equal( + {:known => 'known_value', :controller => ::ContentController, :action => 'index'}, + rec('hello/world') + ) + + assert_nil gen(:known => 'foo') + assert_nil gen({}) + assert_equal '/hello/world', gen(:known => 'known_value', :controller => 'content', :action => 'index') + assert_equal '/hello/world', gen(:known => 'known_value', :extra => 'hi', :controller => 'content', :action => 'index') + assert_equal [:extra], route.extra_keys(:known => 'known_value', :extra => 'hi') + end + + def test_dynamic + route 'hello/:name', :controller => 'content', :action => 'show_person' + + assert_nil rec('hello') + assert_nil rec('foo/bar') + assert_equal({:controller => ::ContentController, :action => 'show_person', :name => 'rails'}, rec('hello/rails')) + assert_equal({:controller => ::ContentController, :action => 'show_person', :name => 'Nicholas Seckar'}, rec('hello/Nicholas+Seckar')) + + assert_nil gen(:controller => 'content', :action => 'show_dude', :name => 'rails') + assert_nil gen(:controller => 'content', :action => 'show_person') + assert_nil gen(:controller => 'admin/user', :action => 'show_person', :name => 'rails') + assert_equal '/hello/rails', gen(:controller => 'content', :action => 'show_person', :name => 'rails') + assert_equal '/hello/Nicholas+Seckar', gen(:controller => 'content', :action => 'show_person', :name => 'Nicholas Seckar') + end + + def test_typical + route ':controller/:action/:id', :action => 'index', :id => nil + assert_nil rec('hello') + assert_nil rec('foo bar') + assert_equal({:controller => ::ContentController, :action => 'index'}, rec('content')) + assert_equal({:controller => ::Admin::UserController, :action => 'index'}, rec('admin/user')) + + assert_equal({:controller => ::Admin::UserController, :action => 'index'}, rec('admin/user/index')) + assert_equal({:controller => ::Admin::UserController, :action => 'list'}, rec('admin/user/list')) + assert_equal({:controller => ::Admin::UserController, :action => 'show', :id => '10'}, rec('admin/user/show/10')) + + assert_equal({:controller => ::ContentController, :action => 'list'}, rec('content/list')) + assert_equal({:controller => ::ContentController, :action => 'show', :id => '10'}, rec('content/show/10')) + + + assert_equal '/content', gen(:controller => 'content', :action => 'index') + assert_equal '/content/list', gen(:controller => 'content', :action => 'list') + assert_equal '/content/show/10', gen(:controller => 'content', :action => 'show', :id => '10') + + assert_equal '/admin/user', gen(:controller => 'admin/user', :action => 'index') + assert_equal '/admin/user', gen(:controller => 'admin/user') + assert_equal '/admin/user', gen({:controller => 'admin/user'}, {:controller => 'content', :action => 'list', :id => '10'}) + assert_equal '/admin/user/show/10', gen(:controller => 'admin/user', :action => 'show', :id => '10') + end +end + +class RouteSetTests < Test::Unit::TestCase + attr_reader :rs + def setup + @rs = ::ActionController::Routing::RouteSet.new + @rs.draw {|m| m.connect ':controller/:action/:id' } + ::ActionController::Routing::NamedRoutes.clear + end + + def test_default_setup + assert_equal({:controller => ::ContentController, :action => 'index'}.stringify_keys, rs.recognize_path(%w(content))) + assert_equal({:controller => ::ContentController, :action => 'list'}.stringify_keys, rs.recognize_path(%w(content list))) + assert_equal({:controller => ::ContentController, :action => 'show', :id => '10'}.stringify_keys, rs.recognize_path(%w(content show 10))) + + assert_equal({:controller => ::Admin::UserController, :action => 'show', :id => '10'}.stringify_keys, rs.recognize_path(%w(admin user show 10))) + + assert_equal ['/admin/user/show/10', []], rs.generate({:controller => 'admin/user', :action => 'show', :id => 10}) + + assert_equal ['/admin/user/show', []], rs.generate({:action => 'show'}, {:controller => 'admin/user', :action => 'list', :id => '10'}) + assert_equal ['/admin/user/list/10', []], rs.generate({}, {:controller => 'admin/user', :action => 'list', :id => '10'}) + + assert_equal ['/admin/stuff', []], rs.generate({:controller => 'stuff'}, {:controller => 'admin/user', :action => 'list', :id => '10'}) + assert_equal ['/stuff', []], rs.generate({:controller => '/stuff'}, {:controller => 'admin/user', :action => 'list', :id => '10'}) + end + + def test_ignores_leading_slash + @rs.draw {|m| m.connect '/:controller/:action/:id'} + test_default_setup + end + + def test_time_recognition + n = 10000 + if RunTimeTests + GC.start + rectime = Benchmark.realtime do + n.times do + rs.recognize_path(%w(content)) + rs.recognize_path(%w(content list)) + rs.recognize_path(%w(content show 10)) + rs.recognize_path(%w(admin user)) + rs.recognize_path(%w(admin user list)) + rs.recognize_path(%w(admin user show 10)) + end + end + puts "\n\nRecognition (RouteSet):" + per_url = rectime / (n * 6) + puts "#{per_url * 1000} ms/url" + puts "#{1 / per_url} url/s\n\n" + end + end + def test_time_generation + n = 5000 + if RunTimeTests + GC.start + pairs = [ + [{:controller => 'content', :action => 'index'}, {:controller => 'content', :action => 'show'}], + [{:controller => 'content'}, {:controller => 'content', :action => 'index'}], + [{:controller => 'content', :action => 'list'}, {:controller => 'content', :action => 'index'}], + [{:controller => 'content', :action => 'show', :id => '10'}, {:controller => 'content', :action => 'list'}], + [{:controller => 'admin/user', :action => 'index'}, {:controller => 'admin/user', :action => 'show'}], + [{:controller => 'admin/user'}, {:controller => 'admin/user', :action => 'index'}], + [{:controller => 'admin/user', :action => 'list'}, {:controller => 'admin/user', :action => 'index'}], + [{:controller => 'admin/user', :action => 'show', :id => '10'}, {:controller => 'admin/user', :action => 'list'}], + ] + p = nil + gentime = Benchmark.realtime do + n.times do + pairs.each {|(a, b)| rs.generate(a, b)} + end + end + + puts "\n\nGeneration (RouteSet): (#{(n * 8)} urls)" + per_url = gentime / (n * 8) + puts "#{per_url * 1000} ms/url" + puts "#{1 / per_url} url/s\n\n" + end + end + + def test_route_with_colon_first + rs.draw do |map| + map.connect '/:controller/:action/:id', :action => 'index', :id => nil + map.connect ':url', :controller => 'tiny_url', :action => 'translate' + end + end + + def test_route_generating_string_literal_in_comparison_warning + old_stderr = $stderr + $stderr = StringIO.new + rs.draw do |map| + map.connect 'subscriptions/:action/:subscription_type', :controller => "subscriptions" + end + assert_equal "", $stderr.string + ensure + $stderr = old_stderr + end + + def test_route_with_regexp_for_controller + rs.draw do |map| + map.connect ':controller/:admintoken/:action/:id', :controller => /admin\/.+/ + map.connect ':controller/:action/:id' + end + assert_equal({:controller => ::Admin::UserController, :admintoken => "foo", :action => "index"}.stringify_keys, + rs.recognize_path(%w(admin user foo))) + assert_equal({:controller => ::ContentController, :action => "foo"}.stringify_keys, + rs.recognize_path(%w(content foo))) + assert_equal ['/admin/user/foo', []], rs.generate(:controller => "admin/user", :admintoken => "foo", :action => "index") + assert_equal ['/content/foo',[]], rs.generate(:controller => "content", :action => "foo") + end + + def test_basic_named_route + rs.home '', :controller => 'content', :action => 'list' + x = setup_for_named_route + assert_equal({:controller => '/content', :action => 'list'}, + x.new.send(:home_url)) + end + + def test_named_route_with_option + rs.page 'page/:title', :controller => 'content', :action => 'show_page' + x = setup_for_named_route + assert_equal({:controller => '/content', :action => 'show_page', :title => 'new stuff'}, + x.new.send(:page_url, :title => 'new stuff')) + end + + def test_named_route_with_default + rs.page 'page/:title', :controller => 'content', :action => 'show_page', :title => 'AboutPage' + x = setup_for_named_route + assert_equal({:controller => '/content', :action => 'show_page', :title => 'AboutPage'}, + x.new.send(:page_url)) + assert_equal({:controller => '/content', :action => 'show_page', :title => 'AboutRails'}, + x.new.send(:page_url, :title => "AboutRails")) + + end + + def setup_for_named_route + x = Class.new + x.send(:define_method, :url_for) {|x| x} + x.send :include, ::ActionController::Routing::NamedRoutes + x + end + + def test_named_route_without_hash + rs.draw do |map| + rs.normal ':controller/:action/:id' + end + end + + def test_named_route_with_regexps + rs.draw do |map| + rs.article 'page/:year/:month/:day/:title', :controller => 'page', :action => 'show', + :year => /^\d+$/, :month => /^\d+$/, :day => /^\d+$/ + rs.connect ':controller/:action/:id' + end + x = setup_for_named_route + assert_equal( + {:controller => '/page', :action => 'show', :title => 'hi'}, + x.new.send(:article_url, :title => 'hi') + ) + assert_equal( + {:controller => '/page', :action => 'show', :title => 'hi', :day => 10, :year => 2005, :month => 6}, + x.new.send(:article_url, :title => 'hi', :day => 10, :year => 2005, :month => 6) + ) + end + + def test_changing_controller + assert_equal ['/admin/stuff/show/10', []], rs.generate( + {:controller => 'stuff', :action => 'show', :id => 10}, + {:controller => 'admin/user', :action => 'index'} + ) + end + + def test_paths_escaped + rs.draw do |map| + rs.path 'file/*path', :controller => 'content', :action => 'show_file' + rs.connect ':controller/:action/:id' + end + results = rs.recognize_path %w(file hello+world how+are+you%3F) + assert results, "Recognition should have succeeded" + assert_equal ['hello world', 'how are you?'], results['path'] + + results = rs.recognize_path %w(file) + assert results, "Recognition should have succeeded" + assert_equal [], results['path'] + end + + def test_non_controllers_cannot_be_matched + rs.draw do + rs.connect ':controller/:action/:id' + end + assert_nil rs.recognize_path(%w(not_a show 10)), "Shouldn't recognize non-controllers as controllers!" + end + + def test_paths_do_not_accept_defaults + assert_raises(ActionController::RoutingError) do + rs.draw do |map| + rs.path 'file/*path', :controller => 'content', :action => 'show_file', :path => %w(fake default) + rs.connect ':controller/:action/:id' + end + end + + rs.draw do |map| + rs.path 'file/*path', :controller => 'content', :action => 'show_file', :path => [] + rs.connect ':controller/:action/:id' + end + end + + def test_backwards + rs.draw do |map| + rs.connect 'page/:id/:action', :controller => 'pages', :action => 'show' + rs.connect ':controller/:action/:id' + end + + assert_equal ['/page/20', []], rs.generate({:id => 20}, {:controller => 'pages'}) + assert_equal ['/page/20', []], rs.generate(:controller => 'pages', :id => 20, :action => 'show') + assert_equal ['/pages/boo', []], rs.generate(:controller => 'pages', :action => 'boo') + end + + def test_route_with_fixnum_default + rs.draw do |map| + rs.connect 'page/:id', :controller => 'content', :action => 'show_page', :id => 1 + rs.connect ':controller/:action/:id' + end + + assert_equal ['/page', []], rs.generate(:controller => 'content', :action => 'show_page') + assert_equal ['/page', []], rs.generate(:controller => 'content', :action => 'show_page', :id => 1) + assert_equal ['/page', []], rs.generate(:controller => 'content', :action => 'show_page', :id => '1') + assert_equal ['/page/10', []], rs.generate(:controller => 'content', :action => 'show_page', :id => 10) + + ctrl = ::ContentController + + assert_equal({'controller' => ctrl, 'action' => 'show_page', 'id' => 1}, rs.recognize_path(%w(page))) + assert_equal({'controller' => ctrl, 'action' => 'show_page', 'id' => '1'}, rs.recognize_path(%w(page 1))) + assert_equal({'controller' => ctrl, 'action' => 'show_page', 'id' => '10'}, rs.recognize_path(%w(page 10))) + end + + def test_action_expiry + assert_equal ['/content', []], rs.generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) + end + + def test_recognition_with_uppercase_controller_name + assert_equal({'controller' => ::ContentController, 'action' => 'index'}, rs.recognize_path(%w(Content))) + assert_equal({'controller' => ::ContentController, 'action' => 'list'}, rs.recognize_path(%w(Content list))) + assert_equal({'controller' => ::ContentController, 'action' => 'show', 'id' => '10'}, rs.recognize_path(%w(Content show 10))) + + assert_equal({'controller' => ::Admin::NewsFeedController, 'action' => 'index'}, rs.recognize_path(%w(Admin NewsFeed))) + assert_equal({'controller' => ::Admin::NewsFeedController, 'action' => 'index'}, rs.recognize_path(%w(Admin News_Feed))) + end + + def test_both_requirement_and_optional + rs.draw do + rs.blog('test/:year', :controller => 'post', :action => 'show', + :defaults => { :year => nil }, + :requirements => { :year => /\d{4}/ } + ) + rs.connect ':controller/:action/:id' + end + + assert_equal ['/test', []], rs.generate(:controller => 'post', :action => 'show') + assert_equal ['/test', []], rs.generate(:controller => 'post', :action => 'show', :year => nil) + + x = setup_for_named_route + assert_equal({:controller => '/post', :action => 'show'}, + x.new.send(:blog_url)) + end + + def test_set_to_nil_forgets + rs.draw do + rs.connect 'pages/:year/:month/:day', :controller => 'content', :action => 'list_pages', :month => nil, :day => nil + rs.connect ':controller/:action/:id' + end + + assert_equal ['/pages/2005', []], + rs.generate(:controller => 'content', :action => 'list_pages', :year => 2005) + assert_equal ['/pages/2005/6', []], + rs.generate(:controller => 'content', :action => 'list_pages', :year => 2005, :month => 6) + assert_equal ['/pages/2005/6/12', []], + rs.generate(:controller => 'content', :action => 'list_pages', :year => 2005, :month => 6, :day => 12) + + assert_equal ['/pages/2005/6/4', []], + rs.generate({:day => 4}, {:controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12'}) + + assert_equal ['/pages/2005/6', []], + rs.generate({:day => nil}, {:controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12'}) + + assert_equal ['/pages/2005', []], + rs.generate({:day => nil, :month => nil}, {:controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12'}) + end + + def test_url_with_no_action_specified + rs.draw do + rs.connect '', :controller => 'content' + rs.connect ':controller/:action/:id' + end + + assert_equal ['/', []], rs.generate(:controller => 'content', :action => 'index') + assert_equal ['/', []], rs.generate(:controller => 'content') + end + + def test_named_url_with_no_action_specified + rs.draw do + rs.root '', :controller => 'content' + rs.connect ':controller/:action/:id' + end + + assert_equal ['/', []], rs.generate(:controller => 'content', :action => 'index') + assert_equal ['/', []], rs.generate(:controller => 'content') + + x = setup_for_named_route + assert_equal({:controller => '/content', :action => 'index'}, + x.new.send(:root_url)) + end + + def test_url_generated_when_forgetting_action + [{:controller => 'content', :action => 'index'}, {:controller => 'content'}].each do |hash| + rs.draw do + rs.root '', hash + rs.connect ':controller/:action/:id' + end + assert_equal ['/', []], rs.generate({:action => nil}, {:controller => 'content', :action => 'hello'}) + assert_equal ['/', []], rs.generate({:controller => 'content'}) + assert_equal ['/content/hi', []], rs.generate({:controller => 'content', :action => 'hi'}) + end + end + + def test_named_route_method + rs.draw do + assert_raises(ArgumentError) { rs.categories 'categories', :controller => 'content', :action => 'categories' } + + rs.named_route :categories, 'categories', :controller => 'content', :action => 'categories' + rs.connect ':controller/:action/:id' + end + + assert_equal ['/categories', []], rs.generate(:controller => 'content', :action => 'categories') + assert_equal ['/content/hi', []], rs.generate({:controller => 'content', :action => 'hi'}) + end + + def test_named_route_helper_array + test_named_route_method + assert_equal [:categories_url, :hash_for_categories_url], ::ActionController::Routing::NamedRoutes::Helpers + end + + def test_nil_defaults + rs.draw do + rs.connect 'journal', + :controller => 'content', + :action => 'list_journal', + :date => nil, :user_id => nil + rs.connect ':controller/:action/:id' + end + + assert_equal ['/journal', []], rs.generate(:controller => 'content', :action => 'list_journal', :date => nil, :user_id => nil) + end +end + +class ControllerComponentTest < Test::Unit::TestCase + + def test_traverse_to_controller_should_not_load_arbitrary_files + load_path = $:.dup + base = File.dirname(File.dirname(File.expand_path(__FILE__))) + $: << File.join(base, 'fixtures') + Object.send :const_set, :RAILS_ROOT, File.join(base, 'fixtures/application_root') + assert_equal nil, ActionController::Routing::ControllerComponent.traverse_to_controller(%w(dont_load pretty please)) + ensure + $:[0..-1] = load_path + Object.send :remove_const, :RAILS_ROOT + end + + def test_traverse_should_not_trip_on_non_module_constants + assert_equal nil, ActionController::Routing::ControllerComponent.traverse_to_controller(%w(admin some_constant a)) + end + + # This is evil, but people do it. + def test_traverse_to_controller_should_pass_thru_classes + load_path = $:.dup + base = File.dirname(File.dirname(File.expand_path(__FILE__))) + $: << File.join(base, 'fixtures') + $: << File.join(base, 'fixtures/application_root/app/controllers') + $: << File.join(base, 'fixtures/application_root/app/models') + Object.send :const_set, :RAILS_ROOT, File.join(base, 'fixtures/application_root') + pair = ActionController::Routing::ControllerComponent.traverse_to_controller(%w(a_class_that_contains_a_controller poorly_placed)) + + # Make sure the container class was loaded properly + assert defined?(AClassThatContainsAController) + assert_kind_of Class, AClassThatContainsAController + assert_equal :you_know_it, AClassThatContainsAController.is_special? + + # Make sure the controller was too + assert_kind_of Array, pair + assert_equal 2, pair[1] + klass = pair.first + assert_kind_of Class, klass + assert_equal :decidedly_so, klass.is_evil? + assert klass.ancestors.include?(ActionController::Base) + assert defined?(AClassThatContainsAController::PoorlyPlacedController) + assert_equal klass, AClassThatContainsAController::PoorlyPlacedController + ensure + $:[0..-1] = load_path + Object.send :remove_const, :RAILS_ROOT + end + + def test_traverse_to_nested_controller + load_path = $:.dup + base = File.dirname(File.dirname(File.expand_path(__FILE__))) + $: << File.join(base, 'fixtures') + $: << File.join(base, 'fixtures/application_root/app/controllers') + Object.send :const_set, :RAILS_ROOT, File.join(base, 'fixtures/application_root') + pair = ActionController::Routing::ControllerComponent.traverse_to_controller(%w(module_that_holds_controllers nested)) + + assert_not_equal nil, pair + + # Make sure that we created a module for the dir + assert defined?(ModuleThatHoldsControllers) + assert_kind_of Module, ModuleThatHoldsControllers + + # Make sure the controller is ok + assert_kind_of Array, pair + assert_equal 2, pair[1] + klass = pair.first + assert_kind_of Class, klass + assert klass.ancestors.include?(ActionController::Base) + assert defined?(ModuleThatHoldsControllers::NestedController) + assert_equal klass, ModuleThatHoldsControllers::NestedController + ensure + $:[0..-1] = load_path + Object.send :remove_const, :RAILS_ROOT + end + +end + +end diff --git a/vendor/rails/actionpack/test/controller/send_file_test.rb b/vendor/rails/actionpack/test/controller/send_file_test.rb new file mode 100644 index 00000000..4c97e2d5 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/send_file_test.rb @@ -0,0 +1,109 @@ +require File.join(File.dirname(__FILE__), '..', 'abstract_unit') + + +module TestFileUtils + def file_name() File.basename(__FILE__) end + def file_path() File.expand_path(__FILE__) end + def file_data() File.open(file_path, 'rb') { |f| f.read } end +end + + +class SendFileController < ActionController::Base + include TestFileUtils + layout "layouts/standard" # to make sure layouts don't interfere + + attr_writer :options + def options() @options ||= {} end + + def file() send_file(file_path, options) end + def data() send_data(file_data, options) end + + def rescue_action(e) raise end +end + +SendFileController.template_root = File.dirname(__FILE__) + "/../fixtures/" + +class SendFileTest < Test::Unit::TestCase + include TestFileUtils + + def setup + @controller = SendFileController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_file_nostream + @controller.options = { :stream => false } + response = nil + assert_nothing_raised { response = process('file') } + assert_not_nil response + assert_kind_of String, response.body + assert_equal file_data, response.body + end + + def test_file_stream + response = nil + assert_nothing_raised { response = process('file') } + assert_not_nil response + assert_kind_of Proc, response.body + + require 'stringio' + output = StringIO.new + output.binmode + assert_nothing_raised { response.body.call(response, output) } + assert_equal file_data, output.string + end + + def test_data + response = nil + assert_nothing_raised { response = process('data') } + assert_not_nil response + + assert_kind_of String, response.body + assert_equal file_data, response.body + end + + # Test that send_file_headers! is setting the correct HTTP headers. + def test_send_file_headers! + options = { + :length => 1, + :type => 'type', + :disposition => 'disposition', + :filename => 'filename' + } + + # Do it a few times: the resulting headers should be identical + # no matter how many times you send with the same options. + # Test resolving Ticket #458. + @controller.headers = {} + @controller.send(:send_file_headers!, options) + @controller.send(:send_file_headers!, options) + @controller.send(:send_file_headers!, options) + + h = @controller.headers + assert_equal 1, h['Content-Length'] + assert_equal 'type', h['Content-Type'] + assert_equal 'disposition; filename="filename"', h['Content-Disposition'] + assert_equal 'binary', h['Content-Transfer-Encoding'] + + # test overriding Cache-Control: no-cache header to fix IE open/save dialog + @controller.headers = { 'Cache-Control' => 'no-cache' } + @controller.send(:send_file_headers!, options) + h = @controller.headers + assert_equal 'private', h['Cache-Control'] + end + + %w(file data).each do |method| + define_method "test_send_#{method}_status" do + @controller.options = { :stream => false, :status => 500 } + assert_nothing_raised { assert_not_nil process(method) } + assert_equal '500', @controller.headers['Status'] + end + + define_method "test_default_send_#{method}_status" do + @controller.options = { :stream => false } + assert_nothing_raised { assert_not_nil process(method) } + assert_equal ActionController::Base::DEFAULT_RENDER_STATUS_CODE, @controller.headers['Status'] + end + end +end diff --git a/vendor/rails/actionpack/test/controller/session_management_test.rb b/vendor/rails/actionpack/test/controller/session_management_test.rb new file mode 100644 index 00000000..c611cb8a --- /dev/null +++ b/vendor/rails/actionpack/test/controller/session_management_test.rb @@ -0,0 +1,94 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class SessionManagementTest < Test::Unit::TestCase + class SessionOffController < ActionController::Base + session :off + + def show + render_text "done" + end + + def tell + render_text "done" + end + end + + class TestController < ActionController::Base + session :off, :only => :show + session :session_secure => true, :except => :show + session :off, :only => :conditional, + :if => Proc.new { |r| r.parameters[:ws] } + + def show + render_text "done" + end + + def tell + render_text "done" + end + + def conditional + render_text ">>>#{params[:ws]}<<<" + end + end + + class SpecializedController < SessionOffController + session :disabled => false, :only => :something + + def something + render_text "done" + end + + def another + render_text "done" + end + end + + def setup + @request, @response = ActionController::TestRequest.new, + ActionController::TestResponse.new + end + + def test_session_off_globally + @controller = SessionOffController.new + get :show + assert_equal false, @request.session_options + get :tell + assert_equal false, @request.session_options + end + + def test_session_off_conditionally + @controller = TestController.new + get :show + assert_equal false, @request.session_options + get :tell + assert_instance_of Hash, @request.session_options + assert @request.session_options[:session_secure] + end + + def test_controller_specialization_overrides_settings + @controller = SpecializedController.new + get :something + assert_instance_of Hash, @request.session_options + get :another + assert_equal false, @request.session_options + end + + def test_session_off_with_if + @controller = TestController.new + get :conditional + assert_instance_of Hash, @request.session_options + get :conditional, :ws => "ws" + assert_equal false, @request.session_options + end + + def test_session_store_setting + ActionController::Base.session_store = :drb_store + assert_equal CGI::Session::DRbStore, ActionController::Base.session_store + + if Object.const_defined?(:ActiveRecord) + ActionController::Base.session_store = :active_record_store + assert_equal CGI::Session::ActiveRecordStore, ActionController::Base.session_store + end + end +end diff --git a/vendor/rails/actionpack/test/controller/test_test.rb b/vendor/rails/actionpack/test/controller/test_test.rb new file mode 100644 index 00000000..88e0c159 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/test_test.rb @@ -0,0 +1,411 @@ +require File.dirname(__FILE__) + '/../abstract_unit' +require File.dirname(__FILE__) + '/fake_controllers' + +class TestTest < Test::Unit::TestCase + class TestController < ActionController::Base + def set_flash + flash["test"] = ">#{flash["test"]}<" + render :text => 'ignore me' + end + + def render_raw_post + raise Test::Unit::AssertionFailedError, "#raw_post is blank" if request.raw_post.blank? + render :text => request.raw_post + end + + def test_params + render :text => params.inspect + end + + def test_uri + render :text => request.request_uri + end + + def test_html_output + render :text => < + + +
      +
        +
      • hello
      • +
      • goodbye
      • +
      +
      +
      +
      + Name: +
      +
      + + +HTML + end + + def test_only_one_param + render :text => (params[:left] && params[:right]) ? "EEP, Both here!" : "OK" + end + + def test_remote_addr + render :text => (request.remote_addr || "not specified") + end + + def test_file_upload + render :text => params[:file].size + end + + def redirect_to_symbol + redirect_to :generate_url, :id => 5 + end + + private + + def rescue_action(e) + raise e + end + + def generate_url(opts) + url_for(opts.merge(:action => "test_uri")) + end + end + + def setup + @controller = TestController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + ActionController::Routing::Routes.reload + end + + def teardown + ActionController::Routing::Routes.reload + end + + def test_raw_post_handling + params = {:page => {:name => 'page name'}, 'some key' => 123} + get :render_raw_post, params.dup + + raw_post = params.map {|k,v| [CGI::escape(k.to_s), CGI::escape(v.to_s)].join('=')}.sort.join('&') + assert_equal raw_post, @response.body + end + + def test_process_without_flash + process :set_flash + assert_equal '><', flash['test'] + end + + def test_process_with_flash + process :set_flash, nil, nil, { "test" => "value" } + assert_equal '>value<', flash['test'] + end + + def test_process_with_request_uri_with_no_params + process :test_uri + assert_equal "/test_test/test/test_uri", @response.body + end + + def test_process_with_request_uri_with_params + process :test_uri, :id => 7 + assert_equal "/test_test/test/test_uri/7", @response.body + end + + def test_process_with_request_uri_with_params_with_explicit_uri + @request.set_REQUEST_URI "/explicit/uri" + process :test_uri, :id => 7 + assert_equal "/explicit/uri", @response.body + end + + def test_multiple_calls + process :test_only_one_param, :left => true + assert_equal "OK", @response.body + process :test_only_one_param, :right => true + assert_equal "OK", @response.body + end + + def test_assert_tag_tag + process :test_html_output + + # there is a 'form' tag + assert_tag :tag => 'form' + # there is not an 'hr' tag + assert_no_tag :tag => 'hr' + end + + def test_assert_tag_attributes + process :test_html_output + + # there is a tag with an 'id' of 'bar' + assert_tag :attributes => { :id => "bar" } + # there is no tag with a 'name' of 'baz' + assert_no_tag :attributes => { :name => "baz" } + end + + def test_assert_tag_parent + process :test_html_output + + # there is a tag with a parent 'form' tag + assert_tag :parent => { :tag => "form" } + # there is no tag with a parent of 'input' + assert_no_tag :parent => { :tag => "input" } + end + + def test_assert_tag_child + process :test_html_output + + # there is a tag with a child 'input' tag + assert_tag :child => { :tag => "input" } + # there is no tag with a child 'strong' tag + assert_no_tag :child => { :tag => "strong" } + end + + def test_assert_tag_ancestor + process :test_html_output + + # there is a 'li' tag with an ancestor having an id of 'foo' + assert_tag :ancestor => { :attributes => { :id => "foo" } }, :tag => "li" + # there is no tag of any kind with an ancestor having an href matching 'foo' + assert_no_tag :ancestor => { :attributes => { :href => /foo/ } } + end + + def test_assert_tag_descendant + process :test_html_output + + # there is a tag with a decendant 'li' tag + assert_tag :descendant => { :tag => "li" } + # there is no tag with a descendant 'html' tag + assert_no_tag :descendant => { :tag => "html" } + end + + def test_assert_tag_sibling + process :test_html_output + + # there is a tag with a sibling of class 'item' + assert_tag :sibling => { :attributes => { :class => "item" } } + # there is no tag with a sibling 'ul' tag + assert_no_tag :sibling => { :tag => "ul" } + end + + def test_assert_tag_after + process :test_html_output + + # there is a tag following a sibling 'div' tag + assert_tag :after => { :tag => "div" } + # there is no tag following a sibling tag with id 'bar' + assert_no_tag :after => { :attributes => { :id => "bar" } } + end + + def test_assert_tag_before + process :test_html_output + + # there is a tag preceeding a tag with id 'bar' + assert_tag :before => { :attributes => { :id => "bar" } } + # there is no tag preceeding a 'form' tag + assert_no_tag :before => { :tag => "form" } + end + + def test_assert_tag_children_count + process :test_html_output + + # there is a tag with 2 children + assert_tag :children => { :count => 2 } + # there is no tag with 4 children + assert_no_tag :children => { :count => 4 } + end + + def test_assert_tag_children_less_than + process :test_html_output + + # there is a tag with less than 5 children + assert_tag :children => { :less_than => 5 } + # there is no 'ul' tag with less than 2 children + assert_no_tag :children => { :less_than => 2 }, :tag => "ul" + end + + def test_assert_tag_children_greater_than + process :test_html_output + + # there is a 'body' tag with more than 1 children + assert_tag :children => { :greater_than => 1 }, :tag => "body" + # there is no tag with more than 10 children + assert_no_tag :children => { :greater_than => 10 } + end + + def test_assert_tag_children_only + process :test_html_output + + # there is a tag containing only one child with an id of 'foo' + assert_tag :children => { :count => 1, + :only => { :attributes => { :id => "foo" } } } + # there is no tag containing only one 'li' child + assert_no_tag :children => { :count => 1, :only => { :tag => "li" } } + end + + def test_assert_tag_content + process :test_html_output + + # the output contains the string "Name" + assert_tag :content => "Name" + # the output does not contain the string "test" + assert_no_tag :content => "test" + end + + def test_assert_tag_multiple + process :test_html_output + + # there is a 'div', id='bar', with an immediate child whose 'action' + # attribute matches the regexp /somewhere/. + assert_tag :tag => "div", :attributes => { :id => "bar" }, + :child => { :attributes => { :action => /somewhere/ } } + + # there is no 'div', id='foo', with a 'ul' child with more than + # 2 "li" children. + assert_no_tag :tag => "div", :attributes => { :id => "foo" }, + :child => { + :tag => "ul", + :children => { :greater_than => 2, + :only => { :tag => "li" } } } + end + + def test_assert_tag_children_without_content + process :test_html_output + + # there is a form tag with an 'input' child which is a self closing tag + assert_tag :tag => "form", + :children => { :count => 1, + :only => { :tag => "input" } } + + # the body tag has an 'a' child which in turn has an 'img' child + assert_tag :tag => "body", + :children => { :count => 1, + :only => { :tag => "a", + :children => { :count => 1, + :only => { :tag => "img" } } } } + end + + def test_assert_generates + assert_generates 'controller/action/5', :controller => 'controller', :action => 'action', :id => '5' + end + + def test_assert_routing + assert_routing 'content', :controller => 'content', :action => 'index' + end + + def test_assert_routing_in_module + assert_routing 'admin/user', :controller => 'admin/user', :action => 'index' + end + + def test_params_passing + get :test_params, :page => {:name => "Page name", :month => '4', :year => '2004', :day => '6'} + parsed_params = eval(@response.body) + assert_equal( + {'controller' => 'test_test/test', 'action' => 'test_params', + 'page' => {'name' => "Page name", 'month' => '4', 'year' => '2004', 'day' => '6'}}, + parsed_params + ) + end + + def test_id_converted_to_string + get :test_params, :id => 20, :foo => Object.new + assert_kind_of String, @request.path_parameters['id'] + end + + def test_array_path_parameter_handled_properly + with_routing do |set| + set.draw do + set.connect 'file/*path', :controller => 'test_test/test', :action => 'test_params' + set.connect ':controller/:action/:id' + end + + get :test_params, :path => ['hello', 'world'] + assert_equal ['hello', 'world'], @request.path_parameters['path'] + assert_equal 'hello/world', @request.path_parameters['path'].to_s + end + end + + def test_assert_realistic_path_parameters + get :test_params, :id => 20, :foo => Object.new + + # All elements of path_parameters should use string keys + @request.path_parameters.keys.each do |key| + assert_kind_of String, key + end + end + + def test_with_routing_places_routes_back + assert ActionController::Routing::Routes + routes_id = ActionController::Routing::Routes.object_id + + begin + with_routing { raise 'fail' } + fail 'Should not be here.' + rescue RuntimeError + end + + assert ActionController::Routing::Routes + assert_equal routes_id, ActionController::Routing::Routes.object_id + end + + def test_remote_addr + get :test_remote_addr + assert_equal "0.0.0.0", @response.body + + @request.remote_addr = "192.0.0.1" + get :test_remote_addr + assert_equal "192.0.0.1", @response.body + end + + def test_header_properly_reset_after_remote_http_request + xhr :get, :test_params + assert_nil @request.env['HTTP_X_REQUESTED_WITH'] + end + + def test_header_properly_reset_after_get_request + get :test_params + @request.recycle! + assert_nil @request.instance_variable_get("@request_method") + end + + %w(controller response request).each do |variable| + %w(get post put delete head process).each do |method| + define_method("test_#{variable}_missing_for_#{method}_raises_error") do + remove_instance_variable "@#{variable}" + begin + send(method, :test_remote_addr) + assert false, "expected RuntimeError, got nothing" + rescue RuntimeError => error + assert true + assert_match %r{@#{variable} is nil}, error.message + rescue => error + assert false, "expected RuntimeError, got #{error.class}" + end + end + end + end + + FILES_DIR = File.dirname(__FILE__) + '/../fixtures/multipart' + + def test_test_uploaded_file + filename = 'mona_lisa.jpg' + path = "#{FILES_DIR}/#{filename}" + content_type = 'image/png' + + file = ActionController::TestUploadedFile.new(path, content_type) + assert_equal filename, file.original_filename + assert_equal content_type, file.content_type + assert_equal file.path, file.local_path + assert_equal File.read(path), file.read + end + + def test_fixture_file_upload + post :test_file_upload, :file => fixture_file_upload(FILES_DIR + "/mona_lisa.jpg", "image/jpg") + assert_equal 159528, @response.body + end + + def test_test_uploaded_file_exception_when_file_doesnt_exist + assert_raise(RuntimeError) { ActionController::TestUploadedFile.new('non_existent_file') } + end + + def test_assert_redirected_to_symbol + get :redirect_to_symbol + assert_redirected_to :generate_url + end +end diff --git a/vendor/rails/actionpack/test/controller/url_rewriter_test.rb b/vendor/rails/actionpack/test/controller/url_rewriter_test.rb new file mode 100644 index 00000000..1219abdd --- /dev/null +++ b/vendor/rails/actionpack/test/controller/url_rewriter_test.rb @@ -0,0 +1,46 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class UrlRewriterTests < Test::Unit::TestCase + def setup + @request = ActionController::TestRequest.new + @params = {} + @rewriter = ActionController::UrlRewriter.new(@request, @params) + end + + def test_simple_build_query_string + assert_query_equal '?x=1&y=2', @rewriter.send(:build_query_string, :x => '1', :y => '2') + end + def test_convert_ints_build_query_string + assert_query_equal '?x=1&y=2', @rewriter.send(:build_query_string, :x => 1, :y => 2) + end + def test_escape_spaces_build_query_string + assert_query_equal '?x=hello+world&y=goodbye+world', @rewriter.send(:build_query_string, :x => 'hello world', :y => 'goodbye world') + end + def test_expand_array_build_query_string + assert_query_equal '?x[]=1&x[]=2', @rewriter.send(:build_query_string, :x => [1, 2]) + end + + def test_escape_spaces_build_query_string_selected_keys + assert_query_equal '?x=hello+world', @rewriter.send(:build_query_string, {:x => 'hello world', :y => 'goodbye world'}, [:x]) + end + + def test_overwrite_params + @params[:controller] = 'hi' + @params[:action] = 'bye' + @params[:id] = '2' + + assert_equal '/hi/hi/2', @rewriter.rewrite(:only_path => true, :overwrite_params => {:action => 'hi'}) + u = @rewriter.rewrite(:only_path => false, :overwrite_params => {:action => 'hi'}) + assert_match %r(/hi/hi/2$), u + end + + + private + def split_query_string(str) + [str[0].chr] + str[1..-1].split(/&/).sort + end + + def assert_query_equal(q1, q2) + assert_equal(split_query_string(q1), split_query_string(q2)) + end +end diff --git a/vendor/rails/actionpack/test/controller/verification_test.rb b/vendor/rails/actionpack/test/controller/verification_test.rb new file mode 100644 index 00000000..a8b2bf44 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/verification_test.rb @@ -0,0 +1,225 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class VerificationTest < Test::Unit::TestCase + class TestController < ActionController::Base + verify :only => :guarded_one, :params => "one", + :redirect_to => { :action => "unguarded" } + + verify :only => :guarded_two, :params => %w( one two ), + :redirect_to => { :action => "unguarded" } + + verify :only => :guarded_with_flash, :params => "one", + :add_flash => { "notice" => "prereqs failed" }, + :redirect_to => { :action => "unguarded" } + + verify :only => :guarded_in_session, :session => "one", + :redirect_to => { :action => "unguarded" } + + verify :only => [:multi_one, :multi_two], :session => %w( one two ), + :redirect_to => { :action => "unguarded" } + + verify :only => :guarded_by_method, :method => :post, + :redirect_to => { :action => "unguarded" } + + verify :only => :guarded_by_xhr, :xhr => true, + :redirect_to => { :action => "unguarded" } + + verify :only => :guarded_by_not_xhr, :xhr => false, + :redirect_to => { :action => "unguarded" } + + before_filter :unconditional_redirect, :only => :two_redirects + verify :only => :two_redirects, :method => :post, + :redirect_to => { :action => "unguarded" } + + verify :only => :must_be_post, :method => :post, :render => { :status => 500, :text => "Must be post"} + + def guarded_one + render :text => "#{@params["one"]}" + end + + def guarded_with_flash + render :text => "#{@params["one"]}" + end + + def guarded_two + render :text => "#{@params["one"]}:#{@params["two"]}" + end + + def guarded_in_session + render :text => "#{@session["one"]}" + end + + def multi_one + render :text => "#{@session["one"]}:#{@session["two"]}" + end + + def multi_two + render :text => "#{@session["two"]}:#{@session["one"]}" + end + + def guarded_by_method + render :text => "#{@request.method}" + end + + def guarded_by_xhr + render :text => "#{@request.xhr?}" + end + + def guarded_by_not_xhr + render :text => "#{@request.xhr?}" + end + + def unguarded + render :text => "#{@params["one"]}" + end + + def two_redirects + render :nothing => true + end + + def must_be_post + render :text => "Was a post!" + end + + protected + def rescue_action(e) raise end + + def unconditional_redirect + redirect_to :action => "unguarded" + end + end + + def setup + @controller = TestController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_guarded_one_with_prereqs + get :guarded_one, :one => "here" + assert_equal "here", @response.body + end + + def test_guarded_one_without_prereqs + get :guarded_one + assert_redirected_to :action => "unguarded" + end + + def test_guarded_with_flash_with_prereqs + get :guarded_with_flash, :one => "here" + assert_equal "here", @response.body + assert_flash_empty + end + + def test_guarded_with_flash_without_prereqs + get :guarded_with_flash + assert_redirected_to :action => "unguarded" + assert_flash_equal "prereqs failed", "notice" + end + + def test_guarded_two_with_prereqs + get :guarded_two, :one => "here", :two => "there" + assert_equal "here:there", @response.body + end + + def test_guarded_two_without_prereqs_one + get :guarded_two, :two => "there" + assert_redirected_to :action => "unguarded" + end + + def test_guarded_two_without_prereqs_two + get :guarded_two, :one => "here" + assert_redirected_to :action => "unguarded" + end + + def test_guarded_two_without_prereqs_both + get :guarded_two + assert_redirected_to :action => "unguarded" + end + + def test_unguarded_with_params + get :unguarded, :one => "here" + assert_equal "here", @response.body + end + + def test_unguarded_without_params + get :unguarded + assert_equal "", @response.body + end + + def test_guarded_in_session_with_prereqs + get :guarded_in_session, {}, "one" => "here" + assert_equal "here", @response.body + end + + def test_guarded_in_session_without_prereqs + get :guarded_in_session + assert_redirected_to :action => "unguarded" + end + + def test_multi_one_with_prereqs + get :multi_one, {}, "one" => "here", "two" => "there" + assert_equal "here:there", @response.body + end + + def test_multi_one_without_prereqs + get :multi_one + assert_redirected_to :action => "unguarded" + end + + def test_multi_two_with_prereqs + get :multi_two, {}, "one" => "here", "two" => "there" + assert_equal "there:here", @response.body + end + + def test_multi_two_without_prereqs + get :multi_two + assert_redirected_to :action => "unguarded" + end + + def test_guarded_by_method_with_prereqs + post :guarded_by_method + assert_equal "post", @response.body + end + + def test_guarded_by_method_without_prereqs + get :guarded_by_method + assert_redirected_to :action => "unguarded" + end + + def test_guarded_by_xhr_with_prereqs + xhr :post, :guarded_by_xhr + assert_equal "true", @response.body + end + + def test_guarded_by_xhr_without_prereqs + get :guarded_by_xhr + assert_redirected_to :action => "unguarded" + end + + def test_guarded_by_not_xhr_with_prereqs + get :guarded_by_not_xhr + assert_equal "false", @response.body + end + + def test_guarded_by_not_xhr_without_prereqs + xhr :post, :guarded_by_not_xhr + assert_redirected_to :action => "unguarded" + end + + def test_guarded_post_and_calls_render_succeeds + post :must_be_post + assert_equal "Was a post!", @response.body + end + + def test_guarded_post_and_calls_render_fails + get :must_be_post + assert_response 500 + assert_equal "Must be post", @response.body + end + + + def test_second_redirect + assert_nothing_raised { get :two_redirects } + end +end diff --git a/vendor/rails/actionpack/test/controller/webservice_test.rb b/vendor/rails/actionpack/test/controller/webservice_test.rb new file mode 100644 index 00000000..9874b209 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/webservice_test.rb @@ -0,0 +1,255 @@ +require File.dirname(__FILE__) + '/../abstract_unit' +require 'stringio' + +class WebServiceTest < Test::Unit::TestCase + + class MockCGI < CGI #:nodoc: + attr_accessor :stdinput, :stdoutput, :env_table + + def initialize(env, data = '') + self.env_table = env + self.stdinput = StringIO.new(data) + self.stdoutput = StringIO.new + super() + end + end + + + class TestController < ActionController::Base + session :off + + def assign_parameters + if params[:full] + render :text => dump_params_keys + else + render :text => (params.keys - ['controller', 'action']).sort.join(", ") + end + end + + def dump_params_keys(hash=params) + hash.keys.sort.inject("") do |s, k| + value = hash[k] + value = Hash === value ? "(#{dump_params_keys(value)})" : "" + s << ", " unless s.empty? + s << "#{k}#{value}" + end + end + + def rescue_action(e) raise end + end + + def setup + @controller = TestController.new + ActionController::Base.param_parsers.clear + ActionController::Base.param_parsers[Mime::XML] = :xml_node + end + + def test_check_parameters + process('GET') + assert_equal '', @controller.response.body + end + + def test_post_xml + process('POST', 'application/xml', 'content...') + + assert_equal 'entry', @controller.response.body + assert @controller.params.has_key?(:entry) + assert_equal 'content...', @controller.params["entry"].summary.node_value + assert_equal 'true', @controller.params["entry"]['attributed'] + end + + def test_put_xml + process('PUT', 'application/xml', 'content...') + + assert_equal 'entry', @controller.response.body + assert @controller.params.has_key?(:entry) + assert_equal 'content...', @controller.params["entry"].summary.node_value + assert_equal 'true', @controller.params["entry"]['attributed'] + end + + def test_register_and_use_yaml + ActionController::Base.param_parsers[Mime::YAML] = Proc.new { |d| YAML.load(d) } + process('POST', 'application/x-yaml', {"entry" => "loaded from yaml"}.to_yaml) + assert_equal 'entry', @controller.response.body + assert @controller.params.has_key?(:entry) + assert_equal 'loaded from yaml', @controller.params["entry"] + end + + def test_register_and_use_yaml_as_symbol + ActionController::Base.param_parsers[Mime::YAML] = :yaml + process('POST', 'application/x-yaml', {"entry" => "loaded from yaml"}.to_yaml) + assert_equal 'entry', @controller.response.body + assert @controller.params.has_key?(:entry) + assert_equal 'loaded from yaml', @controller.params["entry"] + end + + def test_register_and_use_xml_simple + ActionController::Base.param_parsers[Mime::XML] = Proc.new { |data| XmlSimple.xml_in(data, 'ForceArray' => false) } + process('POST', 'application/xml', 'content...SimpleXml' ) + assert_equal 'summary, title', @controller.response.body + assert @controller.params.has_key?(:summary) + assert @controller.params.has_key?(:title) + assert_equal 'content...', @controller.params["summary"] + assert_equal 'SimpleXml', @controller.params["title"] + end + + def test_use_xml_ximple_with_empty_request + ActionController::Base.param_parsers[Mime::XML] = :xml_simple + assert_nothing_raised { process('POST', 'application/xml', "") } + assert_equal "", @controller.response.body + end + + def test_deprecated_request_methods + process('POST', 'application/x-yaml') + assert_equal Mime::YAML, @controller.request.content_type + assert_equal true, @controller.request.post? + assert_equal :yaml, @controller.request.post_format + assert_equal true, @controller.request.yaml_post? + assert_equal false, @controller.request.xml_post? + end + + def test_dasherized_keys_as_xml + ActionController::Base.param_parsers[Mime::XML] = :xml_simple + process('POST', 'application/xml', "\n...\n", true) + assert_equal 'action, controller, first_key(sub_key), full', @controller.response.body + assert_equal "...", @controller.params[:first_key][:sub_key] + end + + def test_typecast_as_xml + ActionController::Base.param_parsers[Mime::XML] = :xml_simple + process('POST', 'application/xml', <<-XML) + + 15 + false + true + 2005-03-17 + 2005-03-17T21:41:07Z + unparsed + 1 + hello + 1974-07-25 + + XML + params = @controller.params + assert_equal 15, params[:data][:a] + assert_equal false, params[:data][:b] + assert_equal true, params[:data][:c] + assert_equal Date.new(2005,3,17), params[:data][:d] + assert_equal Time.utc(2005,3,17,21,41,7), params[:data][:e] + assert_equal "unparsed", params[:data][:f] + assert_equal [1, "hello", Date.new(1974,7,25)], params[:data][:g] + end + + def test_entities_unescaped_as_xml_simple + ActionController::Base.param_parsers[Mime::XML] = :xml_simple + process('POST', 'application/xml', <<-XML) + <foo "bar's" & friends> + XML + assert_equal %(), @controller.params[:data] + end + + def test_dasherized_keys_as_yaml + ActionController::Base.param_parsers[Mime::YAML] = :yaml + process('POST', 'application/x-yaml', "---\nfirst-key:\n sub-key: ...\n", true) + assert_equal 'action, controller, first_key(sub_key), full', @controller.response.body + assert_equal "...", @controller.params[:first_key][:sub_key] + end + + def test_typecast_as_yaml + ActionController::Base.param_parsers[Mime::YAML] = :yaml + process('POST', 'application/x-yaml', <<-YAML) + --- + data: + a: 15 + b: false + c: true + d: 2005-03-17 + e: 2005-03-17T21:41:07Z + f: unparsed + g: + - 1 + - hello + - 1974-07-25 + YAML + params = @controller.params + assert_equal 15, params[:data][:a] + assert_equal false, params[:data][:b] + assert_equal true, params[:data][:c] + assert_equal Date.new(2005,3,17), params[:data][:d] + assert_equal Time.utc(2005,3,17,21,41,7), params[:data][:e] + assert_equal "unparsed", params[:data][:f] + assert_equal [1, "hello", Date.new(1974,7,25)], params[:data][:g] + end + + private + + def process(verb, content_type = 'application/x-www-form-urlencoded', data = '', full=false) + + cgi = MockCGI.new({ + 'REQUEST_METHOD' => verb, + 'CONTENT_TYPE' => content_type, + 'QUERY_STRING' => "action=assign_parameters&controller=webservicetest/test#{"&full=1" if full}", + "REQUEST_URI" => "/", + "HTTP_HOST" => 'testdomain.com', + "CONTENT_LENGTH" => data.size, + "SERVER_PORT" => "80", + "HTTPS" => "off"}, data) + + @controller.send(:process, ActionController::CgiRequest.new(cgi, {}), ActionController::CgiResponse.new(cgi)) + end + +end + + +class XmlNodeTest < Test::Unit::TestCase + def test_all + xn = XmlNode.from_xml(%{ + + + With O'Reilly and Adaptive Path + + + Staying at the Savoy + + + + + + + + + } + ) + assert_equal 'UTF-8', xn.node.document.encoding + assert_equal '1.0', xn.node.document.version + assert_equal 'true', xn['success'] + assert_equal 'response', xn.node_name + assert_equal 'Ajax Summit', xn.page['title'] + assert_equal '1133', xn.page['id'] + assert_equal "With O'Reilly and Adaptive Path", xn.page.description.node_value + assert_equal nil, xn.nonexistent + assert_equal "Staying at the Savoy", xn.page.notes.note.node_value.strip + assert_equal 'Technology', xn.page.tags.tag[0]['name'] + assert_equal 'Travel', xn.page.tags.tag[1][:name] + matches = xn.xpath('//@id').map{ |id| id.to_i } + assert_equal [4, 5, 1020, 1133], matches.sort + matches = xn.xpath('//tag').map{ |tag| tag['name'] } + assert_equal ['Technology', 'Travel'], matches.sort + assert_equal "Ajax Summit", xn.page['title'] + xn.page['title'] = 'Ajax Summit V2' + assert_equal "Ajax Summit V2", xn.page['title'] + assert_equal "Staying at the Savoy", xn.page.notes.note.node_value.strip + xn.page.notes.note.node_value = "Staying at the Ritz" + assert_equal "Staying at the Ritz", xn.page.notes.note.node_value.strip + assert_equal '5', xn.page.tags.tag[1][:id] + xn.page.tags.tag[1]['id'] = '7' + assert_equal '7', xn.page.tags.tag[1]['id'] + end + + + def test_small_entry + node = XmlNode.from_xml('hi') + assert_equal 'hi', node.node_value + end + +end diff --git a/vendor/rails/actionpack/test/fixtures/addresses/list.rhtml b/vendor/rails/actionpack/test/fixtures/addresses/list.rhtml new file mode 100644 index 00000000..c75e01ee --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/addresses/list.rhtml @@ -0,0 +1 @@ +We only need to get this far! diff --git a/vendor/rails/actionpack/test/fixtures/application_root/app/controllers/a_class_that_contains_a_controller/poorly_placed_controller.rb b/vendor/rails/actionpack/test/fixtures/application_root/app/controllers/a_class_that_contains_a_controller/poorly_placed_controller.rb new file mode 100644 index 00000000..11e48da8 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/application_root/app/controllers/a_class_that_contains_a_controller/poorly_placed_controller.rb @@ -0,0 +1,7 @@ +class AClassThatContainsAController::PoorlyPlacedController < ActionController::Base + + def self.is_evil? + :decidedly_so + end + +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/application_root/app/controllers/module_that_holds_controllers/nested_controller.rb b/vendor/rails/actionpack/test/fixtures/application_root/app/controllers/module_that_holds_controllers/nested_controller.rb new file mode 100644 index 00000000..ea6abec2 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/application_root/app/controllers/module_that_holds_controllers/nested_controller.rb @@ -0,0 +1,3 @@ +class ModuleThatHoldsControllers::NestedController < ActionController::Base + +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/application_root/app/models/a_class_that_contains_a_controller.rb b/vendor/rails/actionpack/test/fixtures/application_root/app/models/a_class_that_contains_a_controller.rb new file mode 100644 index 00000000..598e54a1 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/application_root/app/models/a_class_that_contains_a_controller.rb @@ -0,0 +1,7 @@ +class AClassThatContainsAController #often < ActiveRecord::Base + + def self.is_special? + :you_know_it + end + +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/companies.yml b/vendor/rails/actionpack/test/fixtures/companies.yml new file mode 100644 index 00000000..707f72ab --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/companies.yml @@ -0,0 +1,24 @@ +thirty_seven_signals: + id: 1 + name: 37Signals + rating: 4 + +TextDrive: + id: 2 + name: TextDrive + rating: 4 + +PlanetArgon: + id: 3 + name: Planet Argon + rating: 4 + +Google: + id: 4 + name: Google + rating: 4 + +Ionist: + id: 5 + name: Ioni.st + rating: 4 \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/company.rb b/vendor/rails/actionpack/test/fixtures/company.rb new file mode 100644 index 00000000..0d1c29b9 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/company.rb @@ -0,0 +1,9 @@ +class Company < ActiveRecord::Base + attr_protected :rating + set_sequence_name :companies_nonstd_seq + + validates_presence_of :name + def validate + errors.add('rating', 'rating should not be 2') if rating == 2 + end +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/db_definitions/sqlite.sql b/vendor/rails/actionpack/test/fixtures/db_definitions/sqlite.sql new file mode 100644 index 00000000..b4e7539d --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/db_definitions/sqlite.sql @@ -0,0 +1,42 @@ +CREATE TABLE 'companies' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL, + 'rating' INTEGER DEFAULT 1 +); + +CREATE TABLE 'replies' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'content' text, + 'created_at' datetime, + 'updated_at' datetime, + 'topic_id' integer +); + +CREATE TABLE 'topics' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'title' varchar(255), + 'subtitle' varchar(255), + 'content' text, + 'created_at' datetime, + 'updated_at' datetime +); + +CREATE TABLE 'developers' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL, + 'salary' INTEGER DEFAULT 70000, + 'created_at' DATETIME DEFAULT NULL, + 'updated_at' DATETIME DEFAULT NULL +); + +CREATE TABLE 'projects' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL +); + +CREATE TABLE 'developers_projects' ( + 'developer_id' INTEGER NOT NULL, + 'project_id' INTEGER NOT NULL, + 'joined_on' DATE DEFAULT NULL, + 'access_level' INTEGER DEFAULT 1 +); diff --git a/vendor/rails/actionpack/test/fixtures/developer.rb b/vendor/rails/actionpack/test/fixtures/developer.rb new file mode 100644 index 00000000..f5e5b901 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/developer.rb @@ -0,0 +1,7 @@ +class Developer < ActiveRecord::Base + has_and_belongs_to_many :projects +end + +class DeVeLoPeR < ActiveRecord::Base + set_table_name "developers" +end diff --git a/vendor/rails/actionpack/test/fixtures/developers.yml b/vendor/rails/actionpack/test/fixtures/developers.yml new file mode 100644 index 00000000..308bf75d --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/developers.yml @@ -0,0 +1,21 @@ +david: + id: 1 + name: David + salary: 80000 + +jamis: + id: 2 + name: Jamis + salary: 150000 + +<% for digit in 3..10 %> +dev_<%= digit %>: + id: <%= digit %> + name: fixture_<%= digit %> + salary: 100000 +<% end %> + +poor_jamis: + id: 11 + name: Jamis + salary: 9000 \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/developers_projects.yml b/vendor/rails/actionpack/test/fixtures/developers_projects.yml new file mode 100644 index 00000000..cee359c7 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/developers_projects.yml @@ -0,0 +1,13 @@ +david_action_controller: + developer_id: 1 + project_id: 2 + joined_on: 2004-10-10 + +david_active_record: + developer_id: 1 + project_id: 1 + joined_on: 2004-10-10 + +jamis_active_record: + developer_id: 2 + project_id: 1 \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/dont_load.rb b/vendor/rails/actionpack/test/fixtures/dont_load.rb new file mode 100644 index 00000000..6a77d9f7 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/dont_load.rb @@ -0,0 +1,3 @@ +# see routing/controller component tests + +raise Exception, "I should never be loaded" \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/fun/games/hello_world.rhtml b/vendor/rails/actionpack/test/fixtures/fun/games/hello_world.rhtml new file mode 100644 index 00000000..1ebfbe25 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/fun/games/hello_world.rhtml @@ -0,0 +1 @@ +Living in a nested world \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/helpers/abc_helper.rb b/vendor/rails/actionpack/test/fixtures/helpers/abc_helper.rb new file mode 100644 index 00000000..7104ff37 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/helpers/abc_helper.rb @@ -0,0 +1,5 @@ +module AbcHelper + def bare_a() end + def bare_b() end + def bare_c() end +end diff --git a/vendor/rails/actionpack/test/fixtures/helpers/fun/games_helper.rb b/vendor/rails/actionpack/test/fixtures/helpers/fun/games_helper.rb new file mode 100644 index 00000000..bf60d9db --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/helpers/fun/games_helper.rb @@ -0,0 +1,3 @@ +module Fun::GamesHelper + def stratego() "Iz guuut!" end +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/helpers/fun/pdf_helper.rb b/vendor/rails/actionpack/test/fixtures/helpers/fun/pdf_helper.rb new file mode 100644 index 00000000..1890f6c9 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/helpers/fun/pdf_helper.rb @@ -0,0 +1,3 @@ +module Fun::PDFHelper + def foobar() 'baz' end +end diff --git a/vendor/rails/actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.rhtml b/vendor/rails/actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.rhtml new file mode 100644 index 00000000..5f86a7de --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.rhtml @@ -0,0 +1 @@ +controller_name_space/nested.rhtml <%= yield %> \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/layout_tests/layouts/item.rhtml b/vendor/rails/actionpack/test/fixtures/layout_tests/layouts/item.rhtml new file mode 100644 index 00000000..1bc7cbda --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/layout_tests/layouts/item.rhtml @@ -0,0 +1 @@ +item.rhtml <%= yield %> \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/layout_tests/layouts/layout_test.rhtml b/vendor/rails/actionpack/test/fixtures/layout_tests/layouts/layout_test.rhtml new file mode 100644 index 00000000..c0f2642b --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/layout_tests/layouts/layout_test.rhtml @@ -0,0 +1 @@ +layout_test.rhtml <%= yield %> \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/layout_tests/layouts/third_party_template_library.mab b/vendor/rails/actionpack/test/fixtures/layout_tests/layouts/third_party_template_library.mab new file mode 100644 index 00000000..018abfb0 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/layout_tests/layouts/third_party_template_library.mab @@ -0,0 +1 @@ +Mab \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/layout_tests/views/hello.rhtml b/vendor/rails/actionpack/test/fixtures/layout_tests/views/hello.rhtml new file mode 100644 index 00000000..bbccf091 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/layout_tests/views/hello.rhtml @@ -0,0 +1 @@ +hello.rhtml \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/layouts/builder.rxml b/vendor/rails/actionpack/test/fixtures/layouts/builder.rxml new file mode 100644 index 00000000..729af4b8 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/layouts/builder.rxml @@ -0,0 +1,3 @@ +xml.wrapper do + xml << @content_for_layout +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/layouts/standard.rhtml b/vendor/rails/actionpack/test/fixtures/layouts/standard.rhtml new file mode 100644 index 00000000..368764e6 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/layouts/standard.rhtml @@ -0,0 +1 @@ +<%= @content_for_layout %><%= @variable_for_layout %> \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/layouts/talk_from_action.rhtml b/vendor/rails/actionpack/test/fixtures/layouts/talk_from_action.rhtml new file mode 100644 index 00000000..187aab07 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/layouts/talk_from_action.rhtml @@ -0,0 +1,2 @@ +<%= @title || @content_for_title %> +<%= @content_for_layout -%> \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/layouts/yield.rhtml b/vendor/rails/actionpack/test/fixtures/layouts/yield.rhtml new file mode 100644 index 00000000..482dc902 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/layouts/yield.rhtml @@ -0,0 +1,2 @@ +<%= yield :title %> +<%= yield %> diff --git a/vendor/rails/actionpack/test/fixtures/multipart/binary_file b/vendor/rails/actionpack/test/fixtures/multipart/binary_file new file mode 100644 index 00000000..1bcf97f6 Binary files /dev/null and b/vendor/rails/actionpack/test/fixtures/multipart/binary_file differ diff --git a/vendor/rails/actionpack/test/fixtures/multipart/large_text_file b/vendor/rails/actionpack/test/fixtures/multipart/large_text_file new file mode 100644 index 00000000..7f97fb1d --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/multipart/large_text_file @@ -0,0 +1,10 @@ +--AaB03x +Content-Disposition: form-data; name="foo" + +bar +--AaB03x +Content-Disposition: form-data; name="file"; filename="file.txt" +Content-Type: text/plain + +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +--AaB03x-- diff --git a/vendor/rails/actionpack/test/fixtures/multipart/mixed_files b/vendor/rails/actionpack/test/fixtures/multipart/mixed_files new file mode 100644 index 00000000..5eba7a6b Binary files /dev/null and b/vendor/rails/actionpack/test/fixtures/multipart/mixed_files differ diff --git a/vendor/rails/actionpack/test/fixtures/multipart/mona_lisa.jpg b/vendor/rails/actionpack/test/fixtures/multipart/mona_lisa.jpg new file mode 100644 index 00000000..5cf3bef3 Binary files /dev/null and b/vendor/rails/actionpack/test/fixtures/multipart/mona_lisa.jpg differ diff --git a/vendor/rails/actionpack/test/fixtures/multipart/single_parameter b/vendor/rails/actionpack/test/fixtures/multipart/single_parameter new file mode 100644 index 00000000..8962c354 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/multipart/single_parameter @@ -0,0 +1,5 @@ +--AaB03x +Content-Disposition: form-data; name="foo" + +bar +--AaB03x-- diff --git a/vendor/rails/actionpack/test/fixtures/multipart/text_file b/vendor/rails/actionpack/test/fixtures/multipart/text_file new file mode 100644 index 00000000..e0367d68 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/multipart/text_file @@ -0,0 +1,10 @@ +--AaB03x +Content-Disposition: form-data; name="foo" + +bar +--AaB03x +Content-Disposition: form-data; name="file"; filename="file.txt" +Content-Type: text/plain + +contents +--AaB03x-- diff --git a/vendor/rails/actionpack/test/fixtures/project.rb b/vendor/rails/actionpack/test/fixtures/project.rb new file mode 100644 index 00000000..2b53d39e --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/project.rb @@ -0,0 +1,3 @@ +class Project < ActiveRecord::Base + has_and_belongs_to_many :developers, :uniq => true +end diff --git a/vendor/rails/actionpack/test/fixtures/projects.yml b/vendor/rails/actionpack/test/fixtures/projects.yml new file mode 100644 index 00000000..02800c78 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/projects.yml @@ -0,0 +1,7 @@ +action_controller: + id: 2 + name: Active Controller + +active_record: + id: 1 + name: Active Record diff --git a/vendor/rails/actionpack/test/fixtures/public/images/rails.png b/vendor/rails/actionpack/test/fixtures/public/images/rails.png new file mode 100644 index 00000000..b8441f18 Binary files /dev/null and b/vendor/rails/actionpack/test/fixtures/public/images/rails.png differ diff --git a/vendor/rails/actionpack/test/fixtures/replies.yml b/vendor/rails/actionpack/test/fixtures/replies.yml new file mode 100644 index 00000000..284c9c07 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/replies.yml @@ -0,0 +1,13 @@ +witty_retort: + id: 1 + topic_id: 1 + content: Birdman is better! + created_at: <%= 6.hours.ago.to_s(:db) %> + updated_at: nil + +another: + id: 2 + topic_id: 2 + content: Nuh uh! + created_at: <%= 1.hour.ago.to_s(:db) %> + updated_at: nil \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/reply.rb b/vendor/rails/actionpack/test/fixtures/reply.rb new file mode 100644 index 00000000..ea84042b --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/reply.rb @@ -0,0 +1,5 @@ +class Reply < ActiveRecord::Base + belongs_to :topic, :include => [:replies] + + validates_presence_of :content +end diff --git a/vendor/rails/actionpack/test/fixtures/respond_to/all_types_with_layout.rhtml b/vendor/rails/actionpack/test/fixtures/respond_to/all_types_with_layout.rhtml new file mode 100644 index 00000000..84a84049 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/respond_to/all_types_with_layout.rhtml @@ -0,0 +1 @@ +HTML for all_types_with_layout \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/respond_to/all_types_with_layout.rjs b/vendor/rails/actionpack/test/fixtures/respond_to/all_types_with_layout.rjs new file mode 100644 index 00000000..b7aec7c5 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/respond_to/all_types_with_layout.rjs @@ -0,0 +1 @@ +page << "RJS for all_types_with_layout" \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/respond_to/layouts/standard.rhtml b/vendor/rails/actionpack/test/fixtures/respond_to/layouts/standard.rhtml new file mode 100644 index 00000000..fcb28ec7 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/respond_to/layouts/standard.rhtml @@ -0,0 +1 @@ +<%= @content_for_layout %> \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults.rhtml b/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults.rhtml new file mode 100644 index 00000000..6769dd60 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults.rhtml @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults.rjs b/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults.rjs new file mode 100644 index 00000000..469fcd8e --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults.rjs @@ -0,0 +1 @@ +page[:body].visual_effect :highlight \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults.rxml b/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults.rxml new file mode 100644 index 00000000..598d62e2 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults.rxml @@ -0,0 +1 @@ +xml.p "Hello world!" \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.rhtml b/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.rhtml new file mode 100644 index 00000000..6769dd60 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.rhtml @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.rjs b/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.rjs new file mode 100644 index 00000000..469fcd8e --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.rjs @@ -0,0 +1 @@ +page[:body].visual_effect :highlight \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.rxml b/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.rxml new file mode 100644 index 00000000..598d62e2 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.rxml @@ -0,0 +1 @@ +xml.p "Hello world!" \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/scope/test/modgreet.rhtml b/vendor/rails/actionpack/test/fixtures/scope/test/modgreet.rhtml new file mode 100644 index 00000000..8947726e --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/scope/test/modgreet.rhtml @@ -0,0 +1 @@ +

      Beautiful modules!

      \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/_customer.rhtml b/vendor/rails/actionpack/test/fixtures/test/_customer.rhtml new file mode 100644 index 00000000..872d8c44 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/_customer.rhtml @@ -0,0 +1 @@ +Hello: <%= customer.name %> \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/_customer_greeting.rhtml b/vendor/rails/actionpack/test/fixtures/test/_customer_greeting.rhtml new file mode 100644 index 00000000..6acbcb20 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/_customer_greeting.rhtml @@ -0,0 +1 @@ +<%= greeting %>: <%= customer_greeting.name %> \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/_hash_object.rhtml b/vendor/rails/actionpack/test/fixtures/test/_hash_object.rhtml new file mode 100644 index 00000000..037a7368 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/_hash_object.rhtml @@ -0,0 +1 @@ +<%= hash_object[:first_name] %> \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/_partial_only.rhtml b/vendor/rails/actionpack/test/fixtures/test/_partial_only.rhtml new file mode 100644 index 00000000..a44b3eed --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/_partial_only.rhtml @@ -0,0 +1 @@ +only partial \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/_person.rhtml b/vendor/rails/actionpack/test/fixtures/test/_person.rhtml new file mode 100644 index 00000000..b2e56889 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/_person.rhtml @@ -0,0 +1,2 @@ +Second: <%= name %> +Third: <%= @name %> diff --git a/vendor/rails/actionpack/test/fixtures/test/action_talk_to_layout.rhtml b/vendor/rails/actionpack/test/fixtures/test/action_talk_to_layout.rhtml new file mode 100644 index 00000000..36e896da --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/action_talk_to_layout.rhtml @@ -0,0 +1,2 @@ +<% @title = "Talking to the layout" -%> +Action was here! \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/block_content_for.rhtml b/vendor/rails/actionpack/test/fixtures/test/block_content_for.rhtml new file mode 100644 index 00000000..95103373 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/block_content_for.rhtml @@ -0,0 +1,2 @@ +<% block_content_for :title do 'Putting stuff in the title!' end %> +Great stuff! \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/capturing.rhtml b/vendor/rails/actionpack/test/fixtures/test/capturing.rhtml new file mode 100644 index 00000000..1addaa40 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/capturing.rhtml @@ -0,0 +1,4 @@ +<% days = capture do %> + Dreamy days +<% end %> +<%= days %> \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/content_for.rhtml b/vendor/rails/actionpack/test/fixtures/test/content_for.rhtml new file mode 100644 index 00000000..0e47ca8c --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/content_for.rhtml @@ -0,0 +1,2 @@ +<% content_for :title do %>Putting stuff in the title!<% end %> +Great stuff! \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/delete_with_js.rjs b/vendor/rails/actionpack/test/fixtures/test/delete_with_js.rjs new file mode 100644 index 00000000..4b75a955 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/delete_with_js.rjs @@ -0,0 +1,2 @@ +page.remove 'person' +page.visual_effect :highlight, "project-#{@project_id}" diff --git a/vendor/rails/actionpack/test/fixtures/test/dot.directory/render_file_with_ivar.rhtml b/vendor/rails/actionpack/test/fixtures/test/dot.directory/render_file_with_ivar.rhtml new file mode 100644 index 00000000..8b8a4492 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/dot.directory/render_file_with_ivar.rhtml @@ -0,0 +1 @@ +The secret is <%= @secret %> diff --git a/vendor/rails/actionpack/test/fixtures/test/enum_rjs_test.rjs b/vendor/rails/actionpack/test/fixtures/test/enum_rjs_test.rjs new file mode 100644 index 00000000..e3004076 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/enum_rjs_test.rjs @@ -0,0 +1,6 @@ +page.select('.product').each do |value| + page.visual_effect :highlight + page.visual_effect :highlight, value + page.sortable(value, :url => { :action => "order" }) + page.draggable(value) +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/erb_content_for.rhtml b/vendor/rails/actionpack/test/fixtures/test/erb_content_for.rhtml new file mode 100644 index 00000000..c3bdd136 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/erb_content_for.rhtml @@ -0,0 +1,2 @@ +<% erb_content_for :title do %>Putting stuff in the title!<% end %> +Great stuff! \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/greeting.rhtml b/vendor/rails/actionpack/test/fixtures/test/greeting.rhtml new file mode 100644 index 00000000..62fb0293 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/greeting.rhtml @@ -0,0 +1 @@ +

      This is grand!

      diff --git a/vendor/rails/actionpack/test/fixtures/test/hello.rxml b/vendor/rails/actionpack/test/fixtures/test/hello.rxml new file mode 100644 index 00000000..82a4a310 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/hello.rxml @@ -0,0 +1,4 @@ +xml.html do + xml.p "Hello #{@name}" + xml << render_file("test/greeting") +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/hello_world.rhtml b/vendor/rails/actionpack/test/fixtures/test/hello_world.rhtml new file mode 100644 index 00000000..6769dd60 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/hello_world.rhtml @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/hello_world.rxml b/vendor/rails/actionpack/test/fixtures/test/hello_world.rxml new file mode 100644 index 00000000..bffd2191 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/hello_world.rxml @@ -0,0 +1,3 @@ +xml.html do + xml.p "Hello" +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/hello_world_with_layout_false.rhtml b/vendor/rails/actionpack/test/fixtures/test/hello_world_with_layout_false.rhtml new file mode 100644 index 00000000..6769dd60 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/hello_world_with_layout_false.rhtml @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/hello_xml_world.rxml b/vendor/rails/actionpack/test/fixtures/test/hello_xml_world.rxml new file mode 100644 index 00000000..02b14fe8 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/hello_xml_world.rxml @@ -0,0 +1,11 @@ +xml.html do + xml.head do + xml.title "Hello World" + end + + xml.body do + xml.p "abes" + xml.p "monks" + xml.p "wiseguys" + end +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/list.rhtml b/vendor/rails/actionpack/test/fixtures/test/list.rhtml new file mode 100644 index 00000000..cd0ab45d --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/list.rhtml @@ -0,0 +1 @@ +<%= @test_unchanged = 'goodbye' %><%= render_collection_of_partials "customer", @customers %><%= @test_unchanged %> diff --git a/vendor/rails/actionpack/test/fixtures/test/non_erb_block_content_for.rxml b/vendor/rails/actionpack/test/fixtures/test/non_erb_block_content_for.rxml new file mode 100644 index 00000000..6ff6db0f --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/non_erb_block_content_for.rxml @@ -0,0 +1,4 @@ +content_for :title do + 'Putting stuff in the title!' +end +xml << "\nGreat stuff!" \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/potential_conflicts.rhtml b/vendor/rails/actionpack/test/fixtures/test/potential_conflicts.rhtml new file mode 100644 index 00000000..a5e964e3 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/potential_conflicts.rhtml @@ -0,0 +1,4 @@ +First: <%= @name %> +<%= render :partial => "person", :locals => { :name => "Stephan" } -%> +Fourth: <%= @name %> +Fifth: <%= name %> \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/test/render_file_with_ivar.rhtml b/vendor/rails/actionpack/test/fixtures/test/render_file_with_ivar.rhtml new file mode 100644 index 00000000..8b8a4492 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/render_file_with_ivar.rhtml @@ -0,0 +1 @@ +The secret is <%= @secret %> diff --git a/vendor/rails/actionpack/test/fixtures/test/render_file_with_locals.rhtml b/vendor/rails/actionpack/test/fixtures/test/render_file_with_locals.rhtml new file mode 100644 index 00000000..ebe09fae --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/render_file_with_locals.rhtml @@ -0,0 +1 @@ +The secret is <%= secret %> diff --git a/vendor/rails/actionpack/test/fixtures/test/render_to_string_test.rhtml b/vendor/rails/actionpack/test/fixtures/test/render_to_string_test.rhtml new file mode 100644 index 00000000..6e267e86 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/render_to_string_test.rhtml @@ -0,0 +1 @@ +The value of foo is: ::<%= @foo %>:: diff --git a/vendor/rails/actionpack/test/fixtures/test/update_element_with_capture.rhtml b/vendor/rails/actionpack/test/fixtures/test/update_element_with_capture.rhtml new file mode 100644 index 00000000..fa3ef200 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/test/update_element_with_capture.rhtml @@ -0,0 +1,9 @@ +<% replacement_function = update_element_function("products", :action => :update) do %> +

      Product 1

      +

      Product 2

      +<% end %> +<%= javascript_tag(replacement_function) %> + +<% update_element_function("status", :action => :update, :binding => binding) do %> + You bought something! +<% end %> diff --git a/vendor/rails/actionpack/test/fixtures/topic.rb b/vendor/rails/actionpack/test/fixtures/topic.rb new file mode 100644 index 00000000..0929de7e --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/topic.rb @@ -0,0 +1,3 @@ +class Topic < ActiveRecord::Base + has_many :replies, :include => [:user], :dependent => true +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/fixtures/topics.yml b/vendor/rails/actionpack/test/fixtures/topics.yml new file mode 100644 index 00000000..61ea02d7 --- /dev/null +++ b/vendor/rails/actionpack/test/fixtures/topics.yml @@ -0,0 +1,22 @@ +futurama: + id: 1 + title: Isnt futurama awesome? + subtitle: It really is, isnt it. + content: I like futurama + created_at: <%= 1.day.ago.to_s(:db) %> + updated_at: + +harvey_birdman: + id: 2 + title: Harvey Birdman is the king of all men + subtitle: yup + content: It really is + created_at: <%= 2.hours.ago.to_s(:db) %> + updated_at: + +rails: + id: 3 + title: Rails is nice + subtitle: It makes me happy + content: except when I have to hack internals to fix pagination. even then really. + created_at: <%= 20.minutes.ago.to_s(:db) %> diff --git a/vendor/rails/actionpack/test/template/active_record_helper_test.rb b/vendor/rails/actionpack/test/template/active_record_helper_test.rb new file mode 100644 index 00000000..e153e630 --- /dev/null +++ b/vendor/rails/actionpack/test/template/active_record_helper_test.rb @@ -0,0 +1,133 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/date_helper' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/form_helper' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/text_helper' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/tag_helper' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/url_helper' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/form_tag_helper' +# require File.dirname(__FILE__) + '/../../lib/action_view/helpers/active_record_helper' + +class ActiveRecordHelperTest < Test::Unit::TestCase + include ActionView::Helpers::FormHelper + include ActionView::Helpers::ActiveRecordHelper + include ActionView::Helpers::TextHelper + include ActionView::Helpers::TagHelper + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::FormTagHelper + + silence_warnings do + Post = Struct.new("Post", :title, :author_name, :body, :secret, :written_on) + Post.class_eval do + alias_method :title_before_type_cast, :title unless respond_to?(:title_before_type_cast) + alias_method :body_before_type_cast, :body unless respond_to?(:body_before_type_cast) + alias_method :author_name_before_type_cast, :author_name unless respond_to?(:author_name_before_type_cast) + end + Column = Struct.new("Column", :type, :name, :human_name) + end + + def setup + @post = Post.new + def @post.errors + Class.new { + def on(field) field == "author_name" || field == "body" end + def empty?() false end + def count() 1 end + def full_messages() [ "Author name can't be empty" ] end + }.new + end + + def @post.new_record?() true end + def @post.to_param() nil end + + def @post.column_for_attribute(attr_name) + Post.content_columns.select { |column| column.name == attr_name }.first + end + + def Post.content_columns() [ Column.new(:string, "title", "Title"), Column.new(:text, "body", "Body") ] end + + @post.title = "Hello World" + @post.author_name = "" + @post.body = "Back to the hill and over it again!" + @post.secret = 1 + @post.written_on = Date.new(2004, 6, 15) + + @controller = Object.new + def @controller.url_for(options, *parameters_for_method_reference) + options = options.symbolize_keys + + [options[:action], options[:id].to_param].compact.join('/') + end + end + + def test_generic_input_tag + assert_dom_equal( + %(), input("post", "title") + ) + end + + def test_text_area_with_errors + assert_dom_equal( + %(
      ), + text_area("post", "body") + ) + end + + def test_text_field_with_errors + assert_dom_equal( + %(
      ), + text_field("post", "author_name") + ) + end + + def test_form_with_string + assert_dom_equal( + %(


      \n


      ), + form("post") + ) + + class << @post + def new_record?() false end + def to_param() id end + def id() 1 end + end + assert_dom_equal( + %(


      \n


      ), + form("post") + ) + end + + def test_form_with_date + def Post.content_columns() [ Column.new(:date, "written_on", "Written on") ] end + + assert_dom_equal( + %(


      \n\n\n

      ), + form("post") + ) + end + + def test_form_with_datetime + def Post.content_columns() [ Column.new(:datetime, "written_on", "Written on") ] end + @post.written_on = Time.gm(2004, 6, 15, 16, 30) + + assert_dom_equal( + %(


      \n\n\n — \n : \n

      ), + form("post") + ) + end + + def test_error_for_block + assert_dom_equal %(

      1 error prohibited this post from being saved

      There were problems with the following fields:

      • Author name can't be empty
      ), error_messages_for("post") + assert_equal %(

      1 error prohibited this post from being saved

      There were problems with the following fields:

      • Author name can't be empty
      ), error_messages_for("post", :class => "errorDeathByClass", :id => "errorDeathById", :header_tag => "h1") + end + + def test_error_messages_for_handles_nil + assert_equal "", error_messages_for("notthere") + end + + def test_form_with_string_multipart + assert_dom_equal( + %(


      \n


      ), + form("post", :multipart => true) + ) + end +end diff --git a/vendor/rails/actionpack/test/template/asset_tag_helper_test.rb b/vendor/rails/actionpack/test/template/asset_tag_helper_test.rb new file mode 100644 index 00000000..d5a8cef9 --- /dev/null +++ b/vendor/rails/actionpack/test/template/asset_tag_helper_test.rb @@ -0,0 +1,252 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class AssetTagHelperTest < Test::Unit::TestCase + include ActionView::Helpers::TagHelper + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::AssetTagHelper + + def setup + @controller = Class.new do + + attr_accessor :request + + def url_for(options, *parameters_for_method_reference) + "http://www.example.com" + end + + end.new + + @request = Class.new do + def relative_url_root + "" + end + end.new + + @controller.request = @request + + ActionView::Helpers::AssetTagHelper::reset_javascript_include_default + end + + def teardown + Object.send(:remove_const, :RAILS_ROOT) if defined?(RAILS_ROOT) + ENV["RAILS_ASSET_ID"] = nil + end + + AutoDiscoveryToTag = { + %(auto_discovery_link_tag) => %(), + %(auto_discovery_link_tag(:atom)) => %(), + %(auto_discovery_link_tag(:rss, :action => "feed")) => %(), + %(auto_discovery_link_tag(:rss, "http://localhost/feed")) => %(), + %(auto_discovery_link_tag(:rss, {:action => "feed"}, {:title => "My RSS"})) => %(), + %(auto_discovery_link_tag(:rss, {}, {:title => "My RSS"})) => %(), + %(auto_discovery_link_tag(nil, {}, {:type => "text/html"})) => %(), + %(auto_discovery_link_tag(nil, {}, {:title => "No stream.. really", :type => "text/html"})) => %(), + %(auto_discovery_link_tag(:rss, {}, {:title => "My RSS", :type => "text/html"})) => %(), + %(auto_discovery_link_tag(:atom, {}, {:rel => "Not so alternate"})) => %(), + } + + JavascriptPathToTag = { + %(javascript_path("xmlhr")) => %(/javascripts/xmlhr.js), + %(javascript_path("super/xmlhr")) => %(/javascripts/super/xmlhr.js) + } + + JavascriptIncludeToTag = { + %(javascript_include_tag("xmlhr")) => %(), + %(javascript_include_tag("xmlhr", :lang => "vbscript")) => %(), + %(javascript_include_tag("common.javascript", "/elsewhere/cools")) => %(\n), + %(javascript_include_tag(:defaults)) => %(\n\n\n), + %(javascript_include_tag(:defaults, "test")) => %(\n\n\n\n), + %(javascript_include_tag("test", :defaults)) => %(\n\n\n\n) + } + + StylePathToTag = { + %(stylesheet_path("style")) => %(/stylesheets/style.css), + %(stylesheet_path('dir/file')) => %(/stylesheets/dir/file.css), + %(stylesheet_path('/dir/file')) => %(/dir/file.css) + } + + StyleLinkToTag = { + %(stylesheet_link_tag("style")) => %(), + %(stylesheet_link_tag("/dir/file")) => %(), + %(stylesheet_link_tag("dir/file")) => %(), + %(stylesheet_link_tag("style", :media => "all")) => %(), + %(stylesheet_link_tag("random.styles", "/css/stylish")) => %(\n) + } + + ImagePathToTag = { + %(image_path("xml")) => %(/images/xml.png), + } + + ImageLinkToTag = { + %(image_tag("xml")) => %(Xml), + %(image_tag("rss", :alt => "rss syndication")) => %(rss syndication), + %(image_tag("gold", :size => "45x70")) => %(Gold), + %(image_tag("symbolize", "size" => "45x70")) => %(Symbolize), + %(image_tag("http://www.rubyonrails.com/images/rails")) => %(Rails) + } + + def test_auto_discovery + AutoDiscoveryToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_javascript_path + JavascriptPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_javascript_include + JavascriptIncludeToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_register_javascript_include_default + ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'slider' + assert_dom_equal %(\n\n\n\n), javascript_include_tag(:defaults) + ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'lib1', '/elsewhere/blub/lib2' + assert_dom_equal %(\n\n\n\n\n\n), javascript_include_tag(:defaults) + end + + def test_style_path + StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_style_link + StyleLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_image_path + ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_image_tag + ImageLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_timebased_asset_id + Object.send(:const_set, :RAILS_ROOT, File.dirname(__FILE__) + "/../fixtures/") + expected_time = File.stat(File.expand_path(File.dirname(__FILE__) + "/../fixtures/public/images/rails.png")).mtime.to_i.to_s + assert_equal %(Rails), image_tag("rails.png") + end + + def test_skipping_asset_id_on_complete_url + Object.send(:const_set, :RAILS_ROOT, File.dirname(__FILE__) + "/../fixtures/") + assert_equal %(Rails), image_tag("http://www.example.com/rails.png") + end + + def test_preset_asset_id + Object.send(:const_set, :RAILS_ROOT, File.dirname(__FILE__) + "/../fixtures/") + ENV["RAILS_ASSET_ID"] = "4500" + assert_equal %(Rails), image_tag("rails.png") + end +end + +class AssetTagHelperNonVhostTest < Test::Unit::TestCase + include ActionView::Helpers::TagHelper + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::AssetTagHelper + + def setup + @controller = Class.new do + + attr_accessor :request + + def url_for(options, *parameters_for_method_reference) + "http://www.example.com/calloboration/hieraki" + end + + end.new + + @request = Class.new do + def relative_url_root + "/calloboration/hieraki" + end + end.new + + @controller.request = @request + + ActionView::Helpers::AssetTagHelper::reset_javascript_include_default + end + + AutoDiscoveryToTag = { + %(auto_discovery_link_tag(:rss, :action => "feed")) => %(), + %(auto_discovery_link_tag(:atom)) => %(), + %(auto_discovery_link_tag) => %(), + } + + JavascriptPathToTag = { + %(javascript_path("xmlhr")) => %(/calloboration/hieraki/javascripts/xmlhr.js), + } + + JavascriptIncludeToTag = { + %(javascript_include_tag("xmlhr")) => %(), + %(javascript_include_tag("common.javascript", "/elsewhere/cools")) => %(\n), + %(javascript_include_tag(:defaults)) => %(\n\n\n) + } + + StylePathToTag = { + %(stylesheet_path("style")) => %(/calloboration/hieraki/stylesheets/style.css), + } + + StyleLinkToTag = { + %(stylesheet_link_tag("style")) => %(), + %(stylesheet_link_tag("random.styles", "/css/stylish")) => %(\n) + } + + ImagePathToTag = { + %(image_path("xml")) => %(/calloboration/hieraki/images/xml.png), + } + + ImageLinkToTag = { + %(image_tag("xml")) => %(Xml), + %(image_tag("rss", :alt => "rss syndication")) => %(rss syndication), + %(image_tag("gold", :size => "45x70")) => %(Gold), + %(image_tag("http://www.example.com/images/icon.gif")) => %(Icon), + %(image_tag("symbolize", "size" => "45x70")) => %(Symbolize) + } + + def test_auto_discovery + AutoDiscoveryToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_javascript_path + JavascriptPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_javascript_include + JavascriptIncludeToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_register_javascript_include_default + ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'slider' + assert_dom_equal %(\n\n\n\n), javascript_include_tag(:defaults) + ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'lib1', '/elsewhere/blub/lib2' + assert_dom_equal %(\n\n\n\n\n\n), javascript_include_tag(:defaults) + end + + def test_style_path + StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_style_link + StyleLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_image_path + ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_image_tag + ImageLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + # Assigning a default alt tag should not cause an exception to be raised + assert_nothing_raised { image_tag('') } + end + + def test_stylesheet_with_asset_host_already_encoded + ActionController::Base.asset_host = "http://foo.example.com" + result = stylesheet_link_tag("http://bar.example.com/stylesheets/style.css") + assert_dom_equal( + %(), + result) + ensure + ActionController::Base.asset_host = "" + end + +end diff --git a/vendor/rails/actionpack/test/template/benchmark_helper_test.rb b/vendor/rails/actionpack/test/template/benchmark_helper_test.rb new file mode 100644 index 00000000..3c7d9b56 --- /dev/null +++ b/vendor/rails/actionpack/test/template/benchmark_helper_test.rb @@ -0,0 +1,72 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/benchmark_helper' + +class BenchmarkHelperTest < Test::Unit::TestCase + include ActionView::Helpers::BenchmarkHelper + + class MockLogger + attr_reader :logged + + def initialize + @logged = [] + end + + def method_missing(method, *args) + @logged << [method, args] + end + end + + def setup + @logger = MockLogger.new + end + + def test_without_logger_or_block + @logger = nil + assert_nothing_raised { benchmark } + end + + def test_without_block + assert_raise(LocalJumpError) { benchmark } + assert @logger.logged.empty? + end + + def test_without_logger + @logger = nil + i_was_run = false + benchmark { i_was_run = true } + assert !i_was_run + end + + def test_defaults + i_was_run = false + benchmark { i_was_run = true } + assert i_was_run + assert 1, @logger.logged.size + assert_last_logged + end + + def test_with_message + i_was_run = false + benchmark('test_run') { i_was_run = true } + assert i_was_run + assert 1, @logger.logged.size + assert_last_logged 'test_run' + end + + def test_with_message_and_level + i_was_run = false + benchmark('debug_run', :debug) { i_was_run = true } + assert i_was_run + assert 1, @logger.logged.size + assert_last_logged 'debug_run', :debug + end + + private + def assert_last_logged(message = 'Benchmarking', level = :info) + last = @logger.logged.last + assert 2, last.size + assert_equal level, last.first + assert 1, last[1].size + assert last[1][0] =~ /^#{message} \(.*\)$/ + end +end diff --git a/vendor/rails/actionpack/test/template/compiled_templates_test.rb b/vendor/rails/actionpack/test/template/compiled_templates_test.rb new file mode 100644 index 00000000..3bb1e58c --- /dev/null +++ b/vendor/rails/actionpack/test/template/compiled_templates_test.rb @@ -0,0 +1,134 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/date_helper' +require File.dirname(__FILE__) + "/../abstract_unit" + +class CompiledTemplateTests < Test::Unit::TestCase + + def setup + @ct = ActionView::CompiledTemplates.new + @v = Class.new + @v.send :include, @ct + @a = './test_compile_template_a.rhtml' + @b = './test_compile_template_b.rhtml' + @s = './test_compile_template_link.rhtml' + end + def teardown + [@a, @b, @s].each do |f| + `rm #{f}` if File.exist?(f) || File.symlink?(f) + end + end + attr_reader :ct, :v + + def test_name_allocation + hi_world = ct.method_names['hi world'] + hi_sexy = ct.method_names['hi sexy'] + wish_upon_a_star = ct.method_names['I love seeing decent error messages'] + + assert_equal hi_world, ct.method_names['hi world'] + assert_equal hi_sexy, ct.method_names['hi sexy'] + assert_equal wish_upon_a_star, ct.method_names['I love seeing decent error messages'] + assert_equal 3, [hi_world, hi_sexy, wish_upon_a_star].uniq.length + end + + def test_wrap_source + assert_equal( + "def aliased_assignment(value)\nself.value = value\nend", + @ct.wrap_source(:aliased_assignment, [:value], 'self.value = value') + ) + + assert_equal( + "def simple()\nnil\nend", + @ct.wrap_source(:simple, [], 'nil') + ) + end + + def test_compile_source_single_method + selector = ct.compile_source('doubling method', [:a], 'a + a') + assert_equal 2, @v.new.send(selector, 1) + assert_equal 4, @v.new.send(selector, 2) + assert_equal -4, @v.new.send(selector, -2) + assert_equal 0, @v.new.send(selector, 0) + selector + end + + def test_compile_source_two_method + sel1 = test_compile_source_single_method # compile the method in the other test + sel2 = ct.compile_source('doubling method', [:a, :b], 'a + b + a + b') + assert_not_equal sel1, sel2 + + assert_equal 2, @v.new.send(sel1, 1) + assert_equal 4, @v.new.send(sel1, 2) + + assert_equal 6, @v.new.send(sel2, 1, 2) + assert_equal 32, @v.new.send(sel2, 15, 1) + end + + def test_mtime + t1 = Time.now + test_compile_source_single_method + assert (t1..Time.now).include?(ct.mtime('doubling method', [:a])) + end + + def test_compile_time + `echo '#{@a}' > #{@a}; echo '#{@b}' > #{@b}; ln -s #{@a} #{@s}` + + v = ActionView::Base.new + v.base_path = '.' + v.cache_template_loading = false; + + sleep 1 + t = Time.now + v.compile_and_render_template(:rhtml, '', @a) + v.compile_and_render_template(:rhtml, '', @b) + v.compile_and_render_template(:rhtml, '', @s) + a_n = v.method_names[@a] + b_n = v.method_names[@b] + s_n = v.method_names[@s] + # all of the files have changed since last compile + assert v.compile_time[a_n] > t + assert v.compile_time[b_n] > t + assert v.compile_time[s_n] > t + + sleep 1 + t = Time.now + v.compile_and_render_template(:rhtml, '', @a) + v.compile_and_render_template(:rhtml, '', @b) + v.compile_and_render_template(:rhtml, '', @s) + # none of the files have changed since last compile + assert v.compile_time[a_n] < t + assert v.compile_time[b_n] < t + assert v.compile_time[s_n] < t + + `rm #{@s}; ln -s #{@b} #{@s}` + v.compile_and_render_template(:rhtml, '', @a) + v.compile_and_render_template(:rhtml, '', @b) + v.compile_and_render_template(:rhtml, '', @s) + # the symlink has changed since last compile + assert v.compile_time[a_n] < t + assert v.compile_time[b_n] < t + assert v.compile_time[s_n] > t + + sleep 1 + `touch #{@b}` + t = Time.now + v.compile_and_render_template(:rhtml, '', @a) + v.compile_and_render_template(:rhtml, '', @b) + v.compile_and_render_template(:rhtml, '', @s) + # 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 + assert v.compile_time[a_n] < t + assert v.compile_time[b_n] > t + assert v.compile_time[s_n] > t + end +end + +module ActionView + class Base + def compile_time + @@compile_time + end + def method_names + @@method_names + end + end +end diff --git a/vendor/rails/actionpack/test/template/date_helper_test.rb b/vendor/rails/actionpack/test/template/date_helper_test.rb new file mode 100755 index 00000000..0e838c07 --- /dev/null +++ b/vendor/rails/actionpack/test/template/date_helper_test.rb @@ -0,0 +1,630 @@ +require 'test/unit' +require File.dirname(__FILE__) + "/../abstract_unit" + +class DateHelperTest < Test::Unit::TestCase + include ActionView::Helpers::DateHelper + include ActionView::Helpers::FormHelper + + silence_warnings do + Post = Struct.new("Post", :written_on, :updated_at) + end + + def test_distance_in_words + from = Time.mktime(2004, 3, 6, 21, 41, 18) + + assert_equal "less than a minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 25)) + assert_equal "5 minutes", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 46, 25)) + assert_equal "about 1 hour", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 22, 47, 25)) + assert_equal "about 3 hours", distance_of_time_in_words(from, Time.mktime(2004, 3, 7, 0, 41)) + assert_equal "about 4 hours", distance_of_time_in_words(from, Time.mktime(2004, 3, 7, 1, 20)) + assert_equal "2 days", distance_of_time_in_words(from, Time.mktime(2004, 3, 9, 15, 40)) + + # include seconds + assert_equal "less than a minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 19), false) + assert_equal "less than 5 seconds", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 19), true) + assert_equal "less than 10 seconds", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 28), true) + assert_equal "less than 20 seconds", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 38), true) + assert_equal "half a minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 48), true) + assert_equal "less than a minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 42, 17), true) + + assert_equal "1 minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 42, 18), true) + assert_equal "1 minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 42, 28), true) + assert_equal "2 minutes", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 42, 48), true) + + # test to < from + assert_equal "about 4 hours", distance_of_time_in_words(Time.mktime(2004, 3, 7, 1, 20), from) + assert_equal "less than 20 seconds", distance_of_time_in_words(Time.mktime(2004, 3, 6, 21, 41, 38), from, true) + + # test with integers + assert_equal "less than a minute", distance_of_time_in_words(50) + assert_equal "about 1 hour", distance_of_time_in_words(60*60) + + # more cumbersome test with integers + assert_equal "less than a minute", distance_of_time_in_words(0, 50) + assert_equal "about 1 hour", distance_of_time_in_words(60*60, 0) + + end + + def test_distance_in_words_date + start_date = Date.new 1975, 1, 31 + end_date = Date.new 1977, 4, 17 + assert_not_equal("13 minutes", + distance_of_time_in_words(start_date, end_date)) + end + + def test_select_day + expected = %(\n" + + assert_equal expected, select_day(Time.mktime(2003, 8, 16)) + assert_equal expected, select_day(16) + end + + def test_select_day_with_blank + expected = %(\n" + + assert_equal expected, select_day(Time.mktime(2003, 8, 16), :include_blank => true) + assert_equal expected, select_day(16, :include_blank => true) + end + + def test_select_day_nil_with_blank + expected = %(\n" + + assert_equal expected, select_day(nil, :include_blank => true) + end + + def test_select_month + expected = %(\n" + + assert_equal expected, select_month(Time.mktime(2003, 8, 16)) + assert_equal expected, select_month(8) + end + + def test_select_month_with_disabled + expected = %(\n" + + assert_equal expected, select_month(Time.mktime(2003, 8, 16), :disabled => true) + assert_equal expected, select_month(8, :disabled => true) + end + + def test_select_month_with_field_name_override + expected = %(\n" + + assert_equal expected, select_month(Time.mktime(2003, 8, 16), :field_name => 'mois') + assert_equal expected, select_month(8, :field_name => 'mois') + end + + def test_select_month_with_blank + expected = %(\n" + + assert_equal expected, select_month(Time.mktime(2003, 8, 16), :include_blank => true) + assert_equal expected, select_month(8, :include_blank => true) + end + + def test_select_month_nil_with_blank + expected = %(\n" + + assert_equal expected, select_month(nil, :include_blank => true) + end + + def test_select_month_with_numbers + expected = %(\n" + + assert_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_numbers => true) + assert_equal expected, select_month(8, :use_month_numbers => true) + end + + def test_select_month_with_numbers_and_names + expected = %(\n" + + assert_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true) + assert_equal expected, select_month(8, :add_month_numbers => true) + end + + def test_select_month_with_numbers_and_names_with_abbv + expected = %(\n" + + assert_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true, :use_short_month => true) + assert_equal expected, select_month(8, :add_month_numbers => true, :use_short_month => true) + end + + def test_select_month_with_abbv + expected = %(\n" + + assert_equal expected, select_month(Time.mktime(2003, 8, 16), :use_short_month => true) + assert_equal expected, select_month(8, :use_short_month => true) + end + + def test_select_year + expected = %(\n" + + assert_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005) + assert_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005) + end + + def test_select_year_with_disabled + expected = %(\n" + + assert_equal expected, select_year(Time.mktime(2003, 8, 16), :disabled => true, :start_year => 2003, :end_year => 2005) + assert_equal expected, select_year(2003, :disabled => true, :start_year => 2003, :end_year => 2005) + end + + def test_select_year_with_field_name_override + expected = %(\n" + + assert_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :field_name => 'annee') + assert_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005, :field_name => 'annee') + end + + def test_select_year_with_type_discarding + expected = %(\n" + + assert_equal expected, select_year( + Time.mktime(2003, 8, 16), :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005) + assert_equal expected, select_year( + 2003, :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005) + end + + def test_select_year_descending + expected = %(\n" + + assert_equal expected, select_year(Time.mktime(2005, 8, 16), :start_year => 2005, :end_year => 2003) + assert_equal expected, select_year(2005, :start_year => 2005, :end_year => 2003) + end + + def test_select_hour + expected = %(\n" + + assert_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18)) + end + + def test_select_hour_with_disabled + expected = %(\n" + + assert_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true) + end + + def test_select_hour_with_field_name_override + expected = %(\n" + + assert_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'heure') + end + + def test_select_hour_with_blank + expected = %(\n" + + assert_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true) + end + + def test_select_hour_nil_with_blank + expected = %(\n" + + assert_equal expected, select_hour(nil, :include_blank => true) + end + + def test_select_minute + expected = %(\n" + + assert_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18)) + end + + def test_select_minute_with_disabled + expected = %(\n" + + assert_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true) + end + + def test_select_minute_with_field_name_override + expected = %(\n" + + assert_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'minuto') + end + + def test_select_minute_with_blank + expected = %(\n" + + assert_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true) + end + + def test_select_minute_with_blank_and_step + expected = %(\n" + + assert_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), { :include_blank => true , :minute_step => 15 }) + end + + def test_select_minute_nil_with_blank + expected = %(\n" + + assert_equal expected, select_minute(nil, :include_blank => true) + end + + def test_select_minute_nil_with_blank_and_step + expected = %(\n" + + assert_equal expected, select_minute(nil, { :include_blank => true , :minute_step => 15 }) + end + + def test_select_second + expected = %(\n" + + assert_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18)) + end + + def test_select_second_with_disabled + expected = %(\n" + + assert_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true) + end + + def test_select_second_with_field_name_override + expected = %(\n" + + assert_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'segundo') + end + + def test_select_second_with_blank + expected = %(\n" + + assert_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true) + end + + def test_select_second_nil_with_blank + expected = %(\n" + + assert_equal expected, select_second(nil, :include_blank => true) + end + + def test_select_date + expected = %(\n" + + expected << %(\n" + + expected << %(\n" + + assert_equal expected, select_date( + Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]" + ) + end + + def test_select_date_with_disabled + expected = %(\n" + + expected << %(\n" + + expected << %(\n" + + assert_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :disabled => true) + end + + + def test_select_date_with_no_start_year + expected = %(\n" + + expected << %(\n" + + expected << %(\n" + + assert_equal expected, select_date( + Time.mktime(Date.today.year, 8, 16), :end_year => Date.today.year+1, :prefix => "date[first]" + ) + end + + def test_select_date_with_no_end_year + expected = %(\n" + + expected << %(\n" + + expected << %(\n" + + assert_equal expected, select_date( + Time.mktime(2003, 8, 16), :start_year => 2003, :prefix => "date[first]" + ) + end + + def test_select_date_with_no_start_or_end_year + expected = %(\n" + + expected << %(\n" + + expected << %(\n" + + assert_equal expected, select_date( + Time.mktime(Date.today.year, 8, 16), :prefix => "date[first]" + ) + end + + def test_select_time_with_seconds + expected = %(\n" + + expected << %(\n" + + expected << %(\n" + + assert_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true) + end + + def test_select_time_without_seconds + expected = %(\n" + + expected << %(\n" + + assert_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18)) + assert_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => false) + end + + def test_date_select_with_zero_value + expected = %(\n" + + expected << %(\n" + + expected << %(\n" + + assert_equal expected, select_date(0, :start_year => 2003, :end_year => 2005, :prefix => "date[first]") + end + + def test_date_select_within_fields_for + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + _erbout = '' + + fields_for :post, @post do |f| + _erbout.concat f.date_select(:written_on) + end + + expected = "\n" + + "\n" + + "\n" + + assert_dom_equal(expected, _erbout) + end + + def test_datetime_select_within_fields_for + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 16, 35) + + _erbout = '' + + fields_for :post, @post do |f| + _erbout.concat f.datetime_select(:updated_at) + end + + expected = "\n\n\n — \n : \n" + + assert_dom_equal(expected, _erbout) + end + + def test_date_select_with_zero_value_and_no_start_year + expected = %(\n" + + expected << %(\n" + + expected << %(\n" + + assert_equal expected, select_date(0, :end_year => Date.today.year+1, :prefix => "date[first]") + end + + def test_date_select_with_zero_value_and_no_end_year + expected = %(\n" + + expected << %(\n" + + expected << %(\n" + + assert_equal expected, select_date(0, :start_year => 2003, :prefix => "date[first]") + end + + def test_date_select_with_zero_value_and_no_start_and_end_year + expected = %(\n" + + expected << %(\n" + + expected << %(\n" + + assert_equal expected, select_date(0, :prefix => "date[first]") + end + + def test_date_select_with_nil_value_and_no_start_and_end_year + expected = %(\n" + + expected << %(\n" + + expected << %(\n" + + assert_equal expected, select_date(nil, :prefix => "date[first]") + end + + def test_datetime_select_with_nil_value_and_no_start_and_end_year + expected = %(\n" + + expected << %(\n" + + expected << %(\n" + + expected << %(\n" + + expected << %(\n" + + assert_equal expected, select_datetime(nil, :prefix => "date[first]") + end + +end diff --git a/vendor/rails/actionpack/test/template/form_helper_test.rb b/vendor/rails/actionpack/test/template/form_helper_test.rb new file mode 100644 index 00000000..a40a1ff6 --- /dev/null +++ b/vendor/rails/actionpack/test/template/form_helper_test.rb @@ -0,0 +1,423 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class FormHelperTest < Test::Unit::TestCase + include ActionView::Helpers::FormHelper + include ActionView::Helpers::FormTagHelper + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::TagHelper + include ActionView::Helpers::TextHelper + + silence_warnings do + Post = Struct.new("Post", :title, :author_name, :body, :secret, :written_on, :cost) + Post.class_eval do + alias_method :title_before_type_cast, :title unless respond_to?(:title_before_type_cast) + alias_method :body_before_type_cast, :body unless respond_to?(:body_before_type_cast) + alias_method :author_name_before_type_cast, :author_name unless respond_to?(:author_name_before_type_cast) + end + end + + def setup + @post = Post.new + def @post.errors() Class.new{ def on(field) field == "author_name" end }.new end + + def @post.id; 123; end + def @post.id_before_type_cast; 123; end + + @post.title = "Hello World" + @post.author_name = "" + @post.body = "Back to the hill and over it again!" + @post.secret = 1 + @post.written_on = Date.new(2004, 6, 15) + + @controller = Class.new do + attr_reader :url_for_options + def url_for(options, *parameters_for_method_reference) + @url_for_options = options + "http://www.example.com" + end + end + @controller = @controller.new + end + + def test_text_field + assert_dom_equal( + '', text_field("post", "title") + ) + assert_dom_equal( + '', password_field("post", "title") + ) + assert_dom_equal( + '', password_field("person", "name") + ) + end + + def test_text_field_with_escapes + @post.title = "Hello World" + assert_dom_equal( + '', text_field("post", "title") + ) + end + + def test_text_field_with_options + expected = '' + assert_dom_equal expected, text_field("post", "title", "size" => 35) + assert_dom_equal expected, text_field("post", "title", :size => 35) + end + + def test_text_field_assuming_size + expected = '' + assert_dom_equal expected, text_field("post", "title", "maxlength" => 35) + assert_dom_equal expected, text_field("post", "title", :maxlength => 35) + end + + def test_text_field_doesnt_change_param_values + object_name = 'post[]' + expected = '' + assert_equal expected, text_field(object_name, "title") + assert_equal object_name, "post[]" + end + + def test_check_box + assert_dom_equal( + '', + check_box("post", "secret") + ) + @post.secret = 0 + assert_dom_equal( + '', + check_box("post", "secret") + ) + assert_dom_equal( + '', + check_box("post", "secret" ,{"checked"=>"checked"}) + ) + @post.secret = true + assert_dom_equal( + '', + check_box("post", "secret") + ) + end + + def test_check_box_with_explicit_checked_and_unchecked_values + @post.secret = "on" + assert_dom_equal( + '', + check_box("post", "secret", {}, "on", "off") + ) + end + + def test_radio_button + assert_dom_equal('', + radio_button("post", "title", "Hello World") + ) + assert_dom_equal('', + radio_button("post", "title", "Goodbye World") + ) + end + + def test_radio_button_is_checked_with_integers + assert_dom_equal('', + radio_button("post", "secret", "1") + ) + end + + def test_text_area + assert_dom_equal( + '', + text_area("post", "body") + ) + end + + def test_text_area_with_escapes + @post.body = "Back to the hill and over it again!" + assert_dom_equal( + '', + text_area("post", "body") + ) + end + + def test_text_area_with_alternate_value + assert_dom_equal( + '', + text_area("post", "body", :value => 'Testing alternate values.') + ) + end + + def test_date_selects + assert_dom_equal( + '', + text_area("post", "body") + ) + end + + def test_explicit_name + assert_dom_equal( + '', text_field("post", "title", "name" => "dont guess") + ) + assert_dom_equal( + '', + text_area("post", "body", "name" => "really!") + ) + assert_dom_equal( + '', + check_box("post", "secret", "name" => "i mean it") + ) + assert_dom_equal text_field("post", "title", "name" => "dont guess"), + text_field("post", "title", :name => "dont guess") + assert_dom_equal text_area("post", "body", "name" => "really!"), + text_area("post", "body", :name => "really!") + assert_dom_equal check_box("post", "secret", "name" => "i mean it"), + check_box("post", "secret", :name => "i mean it") + end + + def test_explicit_id + assert_dom_equal( + '', text_field("post", "title", "id" => "dont guess") + ) + assert_dom_equal( + '', + text_area("post", "body", "id" => "really!") + ) + assert_dom_equal( + '', + check_box("post", "secret", "id" => "i mean it") + ) + assert_dom_equal text_field("post", "title", "id" => "dont guess"), + text_field("post", "title", :id => "dont guess") + assert_dom_equal text_area("post", "body", "id" => "really!"), + text_area("post", "body", :id => "really!") + assert_dom_equal check_box("post", "secret", "id" => "i mean it"), + check_box("post", "secret", :id => "i mean it") + end + + def test_auto_index + pid = @post.id + assert_dom_equal( + "", text_field("post[]","title") + ) + assert_dom_equal( + "", + text_area("post[]", "body") + ) + assert_dom_equal( + "", + check_box("post[]", "secret") + ) + assert_dom_equal( +"", + radio_button("post[]", "title", "Hello World") + ) + assert_dom_equal("", + radio_button("post[]", "title", "Goodbye World") + ) + end + + def test_form_for + _erbout = '' + + form_for(:post, @post, :html => { :id => 'create-post' }) do |f| + _erbout.concat f.text_field(:title) + _erbout.concat f.text_area(:body) + _erbout.concat f.check_box(:secret) + end + + expected = + "
      " + + "" + + "" + + "" + + "" + + "
      " + + assert_dom_equal expected, _erbout + end + + def test_form_for_without_object + _erbout = '' + + form_for(:post, :html => { :id => 'create-post' }) do |f| + _erbout.concat f.text_field(:title) + _erbout.concat f.text_area(:body) + _erbout.concat f.check_box(:secret) + end + + expected = + "
      " + + "" + + "" + + "" + + "" + + "
      " + + assert_dom_equal expected, _erbout + end + + def test_fields_for + _erbout = '' + + fields_for(:post, @post) do |f| + _erbout.concat f.text_field(:title) + _erbout.concat f.text_area(:body) + _erbout.concat f.check_box(:secret) + end + + expected = + "" + + "" + + "" + + "" + + assert_dom_equal expected, _erbout + end + + def test_fields_for_without_object + _erbout = '' + fields_for(:post) do |f| + _erbout.concat f.text_field(:title) + _erbout.concat f.text_area(:body) + _erbout.concat f.check_box(:secret) + end + + expected = + "" + + "" + + "" + + "" + + assert_dom_equal expected, _erbout + end + + def test_form_builder_does_not_have_form_for_method + assert ! ActionView::Helpers::FormBuilder.instance_methods.include?('form_for') + end + + def test_form_for_and_fields_for + _erbout = '' + + form_for(:post, @post, :html => { :id => 'create-post' }) do |post_form| + _erbout.concat post_form.text_field(:title) + _erbout.concat post_form.text_area(:body) + + fields_for(:parent_post, @post) do |parent_fields| + _erbout.concat parent_fields.check_box(:secret) + end + end + + expected = + "
      " + + "" + + "" + + "" + + "" + + "
      " + + assert_dom_equal expected, _erbout + end + + class LabelledFormBuilder < ActionView::Helpers::FormBuilder + (field_helpers - %w(hidden_field)).each do |selector| + src = <<-END_SRC + def #{selector}(field, *args, &proc) + " " + super + "
      " + end + END_SRC + class_eval src, __FILE__, __LINE__ + end + end + + def test_form_for_with_labelled_builder + _erbout = '' + + form_for(:post, @post, :builder => LabelledFormBuilder) do |f| + _erbout.concat f.text_field(:title) + _erbout.concat f.text_area(:body) + _erbout.concat f.check_box(:secret) + end + + expected = + "
      " + + "
      " + + "
      " + + " " + + "
      " + + "
      " + + assert_dom_equal expected, _erbout + end + + # Perhaps this test should be moved to prototype helper tests. + def test_remote_form_for_with_labelled_builder + self.extend ActionView::Helpers::PrototypeHelper + _erbout = '' + + remote_form_for(:post, @post, :builder => LabelledFormBuilder) do |f| + _erbout.concat f.text_field(:title) + _erbout.concat f.text_area(:body) + _erbout.concat f.check_box(:secret) + end + + expected = + %(
      ) + + "
      " + + "
      " + + " " + + "
      " + + "
      " + + assert_dom_equal expected, _erbout + end + + def test_fields_for_with_labelled_builder + _erbout = '' + + fields_for(:post, @post, :builder => LabelledFormBuilder) do |f| + _erbout.concat f.text_field(:title) + _erbout.concat f.text_area(:body) + _erbout.concat f.check_box(:secret) + end + + expected = + "
      " + + "
      " + + " " + + "
      " + + assert_dom_equal expected, _erbout + end + + def test_form_for_with_html_options_adds_options_to_form_tag + _erbout = '' + + form_for(:post, @post, :html => {:id => 'some_form', :class => 'some_class'}) do |f| end + expected = "
      " + + assert_dom_equal expected, _erbout + end + + def test_form_for_with_string_url_option + _erbout = '' + + form_for(:post, @post, :url => 'http://www.otherdomain.com') do |f| end + + assert_equal 'http://www.otherdomain.com', @controller.url_for_options + end + + def test_form_for_with_hash_url_option + _erbout = '' + + form_for(:post, @post, :url => {:controller => 'controller', :action => 'action'}) do |f| end + + assert_equal 'controller', @controller.url_for_options[:controller] + assert_equal 'action', @controller.url_for_options[:action] + end + + def test_remote_form_for_with_html_options_adds_options_to_form_tag + self.extend ActionView::Helpers::PrototypeHelper + _erbout = '' + + remote_form_for(:post, @post, :html => {:id => 'some_form', :class => 'some_class'}) do |f| end + expected = "
      " + + assert_dom_equal expected, _erbout + end +end diff --git a/vendor/rails/actionpack/test/template/form_options_helper_test.rb b/vendor/rails/actionpack/test/template/form_options_helper_test.rb new file mode 100644 index 00000000..f468b69c --- /dev/null +++ b/vendor/rails/actionpack/test/template/form_options_helper_test.rb @@ -0,0 +1,466 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class MockTimeZone + attr_reader :name + + def initialize( name ) + @name = name + end + + def self.all + [ "A", "B", "C", "D", "E" ].map { |s| new s } + end + + def ==( z ) + z && @name == z.name + end + + def to_s + @name + end +end + +ActionView::Helpers::FormOptionsHelper::TimeZone = MockTimeZone + +class FormOptionsHelperTest < Test::Unit::TestCase + include ActionView::Helpers::FormHelper + include ActionView::Helpers::FormOptionsHelper + + silence_warnings do + Post = Struct.new('Post', :title, :author_name, :body, :secret, :written_on, :category, :origin) + Continent = Struct.new('Continent', :continent_name, :countries) + Country = Struct.new('Country', :country_id, :country_name) + Firm = Struct.new('Firm', :time_zone) + end + + def test_collection_options + @posts = [ + Post.new(" went home", "", "To a little house", "shh!"), + Post.new("Babe went home", "Babe", "To a little house", "shh!"), + Post.new("Cabe went home", "Cabe", "To a little house", "shh!") + ] + + assert_dom_equal( + "\n\n", + options_from_collection_for_select(@posts, "author_name", "title") + ) + end + + + def test_collection_options_with_preselected_value + @posts = [ + Post.new(" went home", "", "To a little house", "shh!"), + Post.new("Babe went home", "Babe", "To a little house", "shh!"), + Post.new("Cabe went home", "Cabe", "To a little house", "shh!") + ] + + assert_dom_equal( + "\n\n", + options_from_collection_for_select(@posts, "author_name", "title", "Babe") + ) + end + + def test_collection_options_with_preselected_value_array + @posts = [ + Post.new(" went home", "", "To a little house", "shh!"), + Post.new("Babe went home", "Babe", "To a little house", "shh!"), + Post.new("Cabe went home", "Cabe", "To a little house", "shh!") + ] + + assert_dom_equal( + "\n\n", + options_from_collection_for_select(@posts, "author_name", "title", [ "Babe", "Cabe" ]) + ) + end + + def test_array_options_for_select + assert_dom_equal( + "\n\n", + options_for_select([ "", "USA", "Sweden" ]) + ) + end + + def test_array_options_for_select_with_selection + assert_dom_equal( + "\n\n", + options_for_select([ "Denmark", "", "Sweden" ], "") + ) + end + + def test_array_options_for_select_with_selection_array + assert_dom_equal( + "\n\n", + options_for_select([ "Denmark", "", "Sweden" ], [ "", "Sweden" ]) + ) + end + + def test_array_options_for_string_include_in_other_string_bug_fix + assert_dom_equal( + "\n", + options_for_select([ "ruby", "rubyonrails" ], "rubyonrails") + ) + assert_dom_equal( + "\n", + options_for_select([ "ruby", "rubyonrails" ], "ruby") + ) + end + + def test_hash_options_for_select + assert_dom_equal( + "\n", + options_for_select({ "$" => "Dollar", "" => "" }) + ) + assert_dom_equal( + "\n", + options_for_select({ "$" => "Dollar", "" => "" }, "Dollar") + ) + assert_dom_equal( + "\n", + options_for_select({ "$" => "Dollar", "" => "" }, [ "Dollar", "" ]) + ) + end + + def test_ducktyped_options_for_select + quack = Struct.new(:first, :last) + assert_dom_equal( + "\n", + options_for_select([quack.new("", ""), quack.new("$", "Dollar")]) + ) + assert_dom_equal( + "\n", + options_for_select([quack.new("", ""), quack.new("$", "Dollar")], "Dollar") + ) + assert_dom_equal( + "\n", + options_for_select([quack.new("", ""), quack.new("$", "Dollar")], ["Dollar", ""]) + ) + end + + def test_html_option_groups_from_collection + @continents = [ + Continent.new("", [Country.new("", ""), Country.new("so", "Somalia")] ), + Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")] ) + ] + + assert_dom_equal( + "\n\n", + option_groups_from_collection_for_select(@continents, "countries", "continent_name", "country_id", "country_name", "dk") + ) + end + + def test_time_zone_options_no_parms + opts = time_zone_options_for_select + assert_dom_equal "\n" + + "\n" + + "\n" + + "\n" + + "", + opts + end + + def test_time_zone_options_with_selected + opts = time_zone_options_for_select( "D" ) + assert_dom_equal "\n" + + "\n" + + "\n" + + "\n" + + "", + opts + end + + def test_time_zone_options_with_unknown_selected + opts = time_zone_options_for_select( "K" ) + assert_dom_equal "\n" + + "\n" + + "\n" + + "\n" + + "", + opts + end + + def test_time_zone_options_with_priority_zones + zones = [ TimeZone.new( "B" ), TimeZone.new( "E" ) ] + opts = time_zone_options_for_select( nil, zones ) + assert_dom_equal "\n" + + "" + + "\n" + + "\n" + + "\n" + + "", + opts + end + + def test_time_zone_options_with_selected_priority_zones + zones = [ TimeZone.new( "B" ), TimeZone.new( "E" ) ] + opts = time_zone_options_for_select( "E", zones ) + assert_dom_equal "\n" + + "" + + "\n" + + "\n" + + "\n" + + "", + opts + end + + def test_time_zone_options_with_unselected_priority_zones + zones = [ TimeZone.new( "B" ), TimeZone.new( "E" ) ] + opts = time_zone_options_for_select( "C", zones ) + assert_dom_equal "\n" + + "" + + "\n" + + "\n" + + "\n" + + "", + opts + end + + def test_select + @post = Post.new + @post.category = "" + assert_dom_equal( + "", + select("post", "category", %w( abe hest)) + ) + end + + def test_select_under_fields_for + @post = Post.new + @post.category = "" + + _erbout = '' + + fields_for :post, @post do |f| + _erbout.concat f.select(:category, %w( abe hest)) + end + + assert_dom_equal( + "", + _erbout + ) + end + + def test_select_with_blank + @post = Post.new + @post.category = "" + assert_dom_equal( + "", + select("post", "category", %w( abe hest), :include_blank => true) + ) + end + + def test_select_with_default_prompt + @post = Post.new + @post.category = "" + assert_dom_equal( + "", + select("post", "category", %w( abe hest), :prompt => true) + ) + end + + def test_select_no_prompt_when_select_has_value + @post = Post.new + @post.category = "" + assert_dom_equal( + "", + select("post", "category", %w( abe hest), :prompt => true) + ) + end + + def test_select_with_given_prompt + @post = Post.new + @post.category = "" + assert_dom_equal( + "", + select("post", "category", %w( abe hest), :prompt => 'The prompt') + ) + end + + def test_select_with_prompt_and_blank + @post = Post.new + @post.category = "" + assert_dom_equal( + "", + select("post", "category", %w( abe hest), :prompt => true, :include_blank => true) + ) + end + + def test_select_with_selected_value + @post = Post.new + @post.category = "" + assert_dom_equal( + "", + select("post", "category", %w( abe hest ), :selected => 'abe') + ) + end + + def test_select_with_selected_nil + @post = Post.new + @post.category = "" + assert_dom_equal( + "", + select("post", "category", %w( abe hest ), :selected => nil) + ) + end + + def test_collection_select + @posts = [ + Post.new(" went home", "", "To a little house", "shh!"), + Post.new("Babe went home", "Babe", "To a little house", "shh!"), + Post.new("Cabe went home", "Cabe", "To a little house", "shh!") + ] + + @post = Post.new + @post.author_name = "Babe" + + assert_dom_equal( + "", + collection_select("post", "author_name", @posts, "author_name", "author_name") + ) + end + + def test_collection_select_under_fields_for + @posts = [ + Post.new(" went home", "", "To a little house", "shh!"), + Post.new("Babe went home", "Babe", "To a little house", "shh!"), + Post.new("Cabe went home", "Cabe", "To a little house", "shh!") + ] + + @post = Post.new + @post.author_name = "Babe" + + _erbout = '' + + fields_for :post, @post do |f| + _erbout.concat f.collection_select(:author_name, @posts, :author_name, :author_name) + end + + assert_dom_equal( + "", + _erbout + ) + end + + def test_collection_select_with_blank_and_style + @posts = [ + Post.new(" went home", "", "To a little house", "shh!"), + Post.new("Babe went home", "Babe", "To a little house", "shh!"), + Post.new("Cabe went home", "Cabe", "To a little house", "shh!") + ] + + @post = Post.new + @post.author_name = "Babe" + + assert_dom_equal( + "", + collection_select("post", "author_name", @posts, "author_name", "author_name", { :include_blank => true }, "style" => "width: 200px") + ) + end + + def test_country_select + @post = Post.new + @post.origin = "Denmark" + assert_dom_equal( + "", + country_select("post", "origin") + ) + end + + def test_time_zone_select + @firm = Firm.new("D") + html = time_zone_select( "firm", "time_zone" ) + assert_dom_equal "", + html + end + + def test_time_zone_select_under_fields_for + @firm = Firm.new("D") + + _erbout = '' + + fields_for :firm, @firm do |f| + _erbout.concat f.time_zone_select(:time_zone) + end + + assert_dom_equal( + "", + _erbout + ) + end + + def test_time_zone_select_with_blank + @firm = Firm.new("D") + html = time_zone_select("firm", "time_zone", nil, :include_blank => true) + assert_dom_equal "", + html + end + + def test_time_zone_select_with_style + @firm = Firm.new("D") + html = time_zone_select("firm", "time_zone", nil, {}, + "style" => "color: red") + assert_dom_equal "", + html + assert_dom_equal html, time_zone_select("firm", "time_zone", nil, {}, + :style => "color: red") + end + + def test_time_zone_select_with_blank_and_style + @firm = Firm.new("D") + html = time_zone_select("firm", "time_zone", nil, + { :include_blank => true }, "style" => "color: red") + assert_dom_equal "", + html + assert_dom_equal html, time_zone_select("firm", "time_zone", nil, + { :include_blank => true }, :style => "color: red") + end + + def test_time_zone_select_with_priority_zones + @firm = Firm.new("D") + zones = [ TimeZone.new("A"), TimeZone.new("D") ] + html = time_zone_select("firm", "time_zone", zones ) + assert_dom_equal "", + html + end +end diff --git a/vendor/rails/actionpack/test/template/form_tag_helper_test.rb b/vendor/rails/actionpack/test/template/form_tag_helper_test.rb new file mode 100644 index 00000000..438cd8f4 --- /dev/null +++ b/vendor/rails/actionpack/test/template/form_tag_helper_test.rb @@ -0,0 +1,108 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class FormTagHelperTest < Test::Unit::TestCase + + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::TagHelper + include ActionView::Helpers::FormTagHelper + + def setup + @controller = Class.new do + def url_for(options, *parameters_for_method_reference) + "http://www.example.com" + end + end + @controller = @controller.new + end + + def test_check_box_tag + actual = check_box_tag "admin" + expected = %() + assert_dom_equal expected, actual + end + + def test_form_tag + actual = form_tag + expected = %(
      ) + assert_dom_equal expected, actual + end + + def test_form_tag_multipart + actual = form_tag({}, { 'multipart' => true }) + expected = %() + assert_dom_equal expected, actual + end + + def test_hidden_field_tag + actual = hidden_field_tag "id", 3 + expected = %() + assert_dom_equal expected, actual + end + + def test_password_field_tag + actual = password_field_tag + expected = %() + assert_dom_equal expected, actual + end + + def test_radio_button_tag + actual = radio_button_tag "people", "david" + expected = %() + assert_dom_equal expected, actual + end + + def test_select_tag + actual = select_tag "people", "" + expected = %() + assert_dom_equal expected, actual + end + + def test_text_area_tag_size_string + actual = text_area_tag "body", "hello world", "size" => "20x40" + expected = %() + assert_dom_equal expected, actual + end + + def test_text_area_tag_size_symbol + actual = text_area_tag "body", "hello world", :size => "20x40" + expected = %() + assert_dom_equal expected, actual + end + + def test_text_field_tag + actual = text_field_tag "title", "Hello!" + expected = %() + assert_dom_equal expected, actual + end + + def test_text_field_tag_class_string + actual = text_field_tag "title", "Hello!", "class" => "admin" + expected = %() + assert_dom_equal expected, actual + end + + def test_boolean_optios + assert_dom_equal %(), check_box_tag("admin", 1, true, 'disabled' => true, :readonly => "yes") + assert_dom_equal %(), check_box_tag("admin", 1, true, :disabled => false, :readonly => nil) + assert_dom_equal %(), select_tag("people", "", :multiple => true) + assert_dom_equal %(), select_tag("people", "", :multiple => nil) + end + + def test_stringify_symbol_keys + actual = text_field_tag "title", "Hello!", :id => "admin" + expected = %() + assert_dom_equal expected, actual + end + + def test_submit_tag + assert_dom_equal( + %(), + submit_tag("Save", :disable_with => "Saving...", :onclick => "alert('hello!')") + ) + end + + def test_pass + assert_equal 1, 1 + end +end + diff --git a/vendor/rails/actionpack/test/template/java_script_macros_helper_test.rb b/vendor/rails/actionpack/test/template/java_script_macros_helper_test.rb new file mode 100644 index 00000000..da168f9e --- /dev/null +++ b/vendor/rails/actionpack/test/template/java_script_macros_helper_test.rb @@ -0,0 +1,106 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class JavaScriptMacrosHelperTest < Test::Unit::TestCase + include ActionView::Helpers::JavaScriptHelper + include ActionView::Helpers::JavaScriptMacrosHelper + + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::TagHelper + include ActionView::Helpers::TextHelper + include ActionView::Helpers::FormHelper + include ActionView::Helpers::CaptureHelper + + def setup + @controller = Class.new do + def url_for(options, *parameters_for_method_reference) + url = "http://www.example.com/" + url << options[:action].to_s if options and options[:action] + url + end + end + @controller = @controller.new + end + + + def test_auto_complete_field + assert_dom_equal %(), + auto_complete_field("some_input", :url => { :action => "autocomplete" }); + assert_dom_equal %(), + auto_complete_field("some_input", :url => { :action => "autocomplete" }, :tokens => ','); + assert_dom_equal %(), + auto_complete_field("some_input", :url => { :action => "autocomplete" }, :tokens => [',']); + assert_dom_equal %(), + auto_complete_field("some_input", :url => { :action => "autocomplete" }, :min_chars => 3); + assert_dom_equal %(), + auto_complete_field("some_input", :url => { :action => "autocomplete" }, :on_hide => "function(element, update){alert('me');}"); + assert_dom_equal %(), + auto_complete_field("some_input", :url => { :action => "autocomplete" }, :frequency => 2); + assert_dom_equal %(), + auto_complete_field("some_input", :url => { :action => "autocomplete" }, + :after_update_element => "function(element,value){alert('You have chosen: '+value)}"); + end + + def test_auto_complete_result + result = [ { :title => 'test1' }, { :title => 'test2' } ] + assert_equal %(
      • test1
      • test2
      ), + auto_complete_result(result, :title) + assert_equal %(
      • test1
      • test2
      ), + auto_complete_result(result, :title, "est") + + resultuniq = [ { :title => 'test1' }, { :title => 'test1' } ] + assert_equal %(
      • test1
      ), + auto_complete_result(resultuniq, :title, "est") + end + + def test_text_field_with_auto_complete + assert_match " + + + +<%= @content_for_layout %> + + + diff --git a/vendor/rails/actionwebservice/lib/action_web_service/templates/scaffolds/methods.rhtml b/vendor/rails/actionwebservice/lib/action_web_service/templates/scaffolds/methods.rhtml new file mode 100644 index 00000000..60dfe23f --- /dev/null +++ b/vendor/rails/actionwebservice/lib/action_web_service/templates/scaffolds/methods.rhtml @@ -0,0 +1,6 @@ +<% @scaffold_container.services.each do |service| %> + +

      API Methods for <%= service %>

      + <%= service_method_list(service) %> + +<% end %> diff --git a/vendor/rails/actionwebservice/lib/action_web_service/templates/scaffolds/parameters.rhtml b/vendor/rails/actionwebservice/lib/action_web_service/templates/scaffolds/parameters.rhtml new file mode 100644 index 00000000..ce755f78 --- /dev/null +++ b/vendor/rails/actionwebservice/lib/action_web_service/templates/scaffolds/parameters.rhtml @@ -0,0 +1,29 @@ +

      Method Invocation Details for <%= @scaffold_service %>#<%= @scaffold_method.public_name %>

      + +<%= form_tag :action => @scaffold_action_name + '_submit' %> +<%= hidden_field_tag "service", @scaffold_service.name %> +<%= hidden_field_tag "method", @scaffold_method.public_name %> + +

      +
      +<%= select_tag 'protocol', options_for_select([['SOAP', 'soap'], ['XML-RPC', 'xmlrpc']], @params['protocol']) %> +

      + +<% if @scaffold_method.expects %> + +Method Parameters:
      +<% @scaffold_method.expects.each_with_index do |type, i| %> +

      +
      + <%= method_parameter_input_fields(@scaffold_method, type, "method_params", i) %> +

      +<% end %> + +<% end %> + +<%= submit_tag "Invoke" %> +<%= end_form_tag %> + +

      +<%= link_to "Back", :action => @scaffold_action_name %> +

      diff --git a/vendor/rails/actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml b/vendor/rails/actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml new file mode 100644 index 00000000..5317688f --- /dev/null +++ b/vendor/rails/actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml @@ -0,0 +1,30 @@ +

      Method Invocation Result for <%= @scaffold_service %>#<%= @scaffold_method.public_name %>

      + +

      +Invocation took <%= '%f' % @method_elapsed %> seconds +

      + +

      +Return Value:
      +

      +<%= h @method_return_value.inspect %>
      +
      +

      + +

      +Request XML:
      +

      +<%= h @method_request_xml %>
      +
      +

      + +

      +Response XML:
      +

      +<%= h @method_response_xml %>
      +
      +

      + +

      +<%= link_to "Back", :action => @scaffold_action_name + '_method_params', :method => @scaffold_method.public_name, :service => @scaffold_service.name %> +

      diff --git a/vendor/rails/actionwebservice/lib/action_web_service/test_invoke.rb b/vendor/rails/actionwebservice/lib/action_web_service/test_invoke.rb new file mode 100644 index 00000000..e4469851 --- /dev/null +++ b/vendor/rails/actionwebservice/lib/action_web_service/test_invoke.rb @@ -0,0 +1,110 @@ +require 'test/unit' + +module Test # :nodoc: + module Unit # :nodoc: + class TestCase # :nodoc: + private + # invoke the specified API method + def invoke_direct(method_name, *args) + prepare_request('api', 'api', method_name, *args) + @controller.process(@request, @response) + decode_rpc_response + end + alias_method :invoke, :invoke_direct + + # invoke the specified API method on the specified service + def invoke_delegated(service_name, method_name, *args) + prepare_request(service_name.to_s, service_name, method_name, *args) + @controller.process(@request, @response) + decode_rpc_response + end + + # invoke the specified layered API method on the correct service + def invoke_layered(service_name, method_name, *args) + prepare_request('api', service_name, method_name, *args) + @controller.process(@request, @response) + decode_rpc_response + end + + # ---------------------- internal --------------------------- + + def prepare_request(action, service_name, api_method_name, *args) + @request.recycle! + @request.request_parameters['action'] = action + @request.env['REQUEST_METHOD'] = 'POST' + @request.env['HTTP_CONTENT_TYPE'] = 'text/xml' + @request.env['RAW_POST_DATA'] = encode_rpc_call(service_name, api_method_name, *args) + case protocol + when ActionWebService::Protocol::Soap::SoapProtocol + soap_action = "/#{@controller.controller_name}/#{service_name}/#{public_method_name(service_name, api_method_name)}" + @request.env['HTTP_SOAPACTION'] = soap_action + when ActionWebService::Protocol::XmlRpc::XmlRpcProtocol + @request.env.delete('HTTP_SOAPACTION') + end + end + + def encode_rpc_call(service_name, api_method_name, *args) + case @controller.web_service_dispatching_mode + when :direct + api = @controller.class.web_service_api + when :delegated, :layered + api = @controller.web_service_object(service_name.to_sym).class.web_service_api + end + protocol.register_api(api) + method = api.api_methods[api_method_name.to_sym] + raise ArgumentError, "wrong number of arguments for rpc call (#{args.length} for #{method.expects.length})" unless args.length == method.expects.length + protocol.encode_request(public_method_name(service_name, api_method_name), args.dup, method.expects) + end + + def decode_rpc_response + public_method_name, return_value = protocol.decode_response(@response.body) + exception = is_exception?(return_value) + raise exception if exception + return_value + end + + def public_method_name(service_name, api_method_name) + public_name = service_api(service_name).public_api_method_name(api_method_name) + if @controller.web_service_dispatching_mode == :layered && protocol.is_a?(ActionWebService::Protocol::XmlRpc::XmlRpcProtocol) + '%s.%s' % [service_name.to_s, public_name] + else + public_name + end + end + + def service_api(service_name) + case @controller.web_service_dispatching_mode + when :direct + @controller.class.web_service_api + when :delegated, :layered + @controller.web_service_object(service_name.to_sym).class.web_service_api + end + end + + def protocol + if @protocol.nil? + @protocol ||= ActionWebService::Protocol::Soap::SoapProtocol.create(@controller) + else + case @protocol + when :xmlrpc + @protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.create(@controller) + when :soap + @protocol = ActionWebService::Protocol::Soap::SoapProtocol.create(@controller) + else + @protocol + end + end + end + + def is_exception?(obj) + case protocol + when :soap, ActionWebService::Protocol::Soap::SoapProtocol + (obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && \ + obj.detail.cause.is_a?(Exception)) ? obj.detail.cause : nil + when :xmlrpc, ActionWebService::Protocol::XmlRpc::XmlRpcProtocol + obj.is_a?(XMLRPC::FaultException) ? obj : nil + end + end + end + end +end diff --git a/vendor/rails/actionwebservice/lib/action_web_service/version.rb b/vendor/rails/actionwebservice/lib/action_web_service/version.rb new file mode 100644 index 00000000..cc1db831 --- /dev/null +++ b/vendor/rails/actionwebservice/lib/action_web_service/version.rb @@ -0,0 +1,9 @@ +module ActionWebService + module VERSION #:nodoc: + MAJOR = 1 + MINOR = 1 + TINY = 6 + + STRING = [MAJOR, MINOR, TINY].join('.') + end +end diff --git a/vendor/rails/actionwebservice/setup.rb b/vendor/rails/actionwebservice/setup.rb new file mode 100644 index 00000000..9ab880d3 --- /dev/null +++ b/vendor/rails/actionwebservice/setup.rb @@ -0,0 +1,1379 @@ +# +# setup.rb +# +# Copyright (c) 2000-2004 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. + +# + +unless Enumerable.method_defined?(:map) # Ruby 1.4.6 + module Enumerable + alias map collect + end +end + +unless File.respond_to?(:read) # Ruby 1.6 + def File.read(fname) + open(fname) {|f| + return f.read + } + end +end + +def File.binread(fname) + open(fname, 'rb') {|f| + return f.read + } +end + +# for corrupted windows stat(2) +def File.dir?(path) + File.directory?((path[-1,1] == '/') ? path : path + '/') +end + + +class SetupError < StandardError; end + +def setup_rb_error(msg) + raise SetupError, msg +end + +# +# Config +# + +if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } + ARGV.delete(arg) + require arg.split(/=/, 2)[1] + $".push 'rbconfig.rb' +else + require 'rbconfig' +end + +def multipackage_install? + FileTest.directory?(File.dirname($0) + '/packages') +end + + +class ConfigItem + def initialize(name, template, default, desc) + @name = name.freeze + @template = template + @value = default + @default = default.dup.freeze + @description = desc + end + + attr_reader :name + attr_reader :description + + attr_accessor :default + alias help_default default + + def help_opt + "--#{@name}=#{@template}" + end + + def value + @value + end + + def eval(table) + @value.gsub(%r<\$([^/]+)>) { table[$1] } + end + + def set(val) + @value = check(val) + end + + private + + def check(val) + setup_rb_error "config: --#{name} requires argument" unless val + val + end +end + +class BoolItem < ConfigItem + def config_type + 'bool' + end + + def help_opt + "--#{@name}" + end + + private + + def check(val) + return 'yes' unless val + unless /\A(y(es)?|n(o)?|t(rue)?|f(alse))\z/i =~ val + setup_rb_error "config: --#{@name} accepts only yes/no for argument" + end + (/\Ay(es)?|\At(rue)/i =~ value) ? 'yes' : 'no' + end +end + +class PathItem < ConfigItem + def config_type + 'path' + end + + private + + def check(path) + setup_rb_error "config: --#{@name} requires argument" unless path + path[0,1] == '$' ? path : File.expand_path(path) + end +end + +class ProgramItem < ConfigItem + def config_type + 'program' + end +end + +class SelectItem < ConfigItem + def initialize(name, template, default, desc) + super + @ok = template.split('/') + end + + def config_type + 'select' + end + + private + + def check(val) + unless @ok.include?(val.strip) + setup_rb_error "config: use --#{@name}=#{@template} (#{val})" + end + val.strip + end +end + +class PackageSelectionItem < ConfigItem + def initialize(name, template, default, help_default, desc) + super name, template, default, desc + @help_default = help_default + end + + attr_reader :help_default + + def config_type + 'package' + end + + private + + def check(val) + unless File.dir?("packages/#{val}") + setup_rb_error "config: no such package: #{val}" + end + val + end +end + +class ConfigTable_class + + def initialize(items) + @items = items + @table = {} + items.each do |i| + @table[i.name] = i + end + ALIASES.each do |ali, name| + @table[ali] = @table[name] + end + end + + include Enumerable + + def each(&block) + @items.each(&block) + end + + def key?(name) + @table.key?(name) + end + + def lookup(name) + @table[name] or raise ArgumentError, "no such config item: #{name}" + end + + def add(item) + @items.push item + @table[item.name] = item + end + + def remove(name) + item = lookup(name) + @items.delete_if {|i| i.name == name } + @table.delete_if {|name, i| i.name == name } + item + end + + def new + dup() + end + + def savefile + '.config' + end + + def load + begin + t = dup() + File.foreach(savefile()) do |line| + k, v = *line.split(/=/, 2) + t[k] = v.strip + end + t + rescue Errno::ENOENT + setup_rb_error $!.message + "#{File.basename($0)} config first" + end + end + + def save + @items.each {|i| i.value } + File.open(savefile(), 'w') {|f| + @items.each do |i| + f.printf "%s=%s\n", i.name, i.value if i.value + end + } + end + + def [](key) + lookup(key).eval(self) + end + + def []=(key, val) + lookup(key).set val + end + +end + +c = ::Config::CONFIG + +rubypath = c['bindir'] + '/' + c['ruby_install_name'] + +major = c['MAJOR'].to_i +minor = c['MINOR'].to_i +teeny = c['TEENY'].to_i +version = "#{major}.#{minor}" + +# ruby ver. >= 1.4.4? +newpath_p = ((major >= 2) or + ((major == 1) and + ((minor >= 5) or + ((minor == 4) and (teeny >= 4))))) + +if c['rubylibdir'] + # V < 1.6.3 + _stdruby = c['rubylibdir'] + _siteruby = c['sitedir'] + _siterubyver = c['sitelibdir'] + _siterubyverarch = c['sitearchdir'] +elsif newpath_p + # 1.4.4 <= V <= 1.6.3 + _stdruby = "$prefix/lib/ruby/#{version}" + _siteruby = c['sitedir'] + _siterubyver = "$siteruby/#{version}" + _siterubyverarch = "$siterubyver/#{c['arch']}" +else + # V < 1.4.4 + _stdruby = "$prefix/lib/ruby/#{version}" + _siteruby = "$prefix/lib/ruby/#{version}/site_ruby" + _siterubyver = _siteruby + _siterubyverarch = "$siterubyver/#{c['arch']}" +end +libdir = '-* dummy libdir *-' +stdruby = '-* dummy rubylibdir *-' +siteruby = '-* dummy site_ruby *-' +siterubyver = '-* dummy site_ruby version *-' +parameterize = lambda {|path| + path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix')\ + .sub(/\A#{Regexp.quote(libdir)}/, '$libdir')\ + .sub(/\A#{Regexp.quote(stdruby)}/, '$stdruby')\ + .sub(/\A#{Regexp.quote(siteruby)}/, '$siteruby')\ + .sub(/\A#{Regexp.quote(siterubyver)}/, '$siterubyver') +} +libdir = parameterize.call(c['libdir']) +stdruby = parameterize.call(_stdruby) +siteruby = parameterize.call(_siteruby) +siterubyver = parameterize.call(_siterubyver) +siterubyverarch = parameterize.call(_siterubyverarch) + +if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } + makeprog = arg.sub(/'/, '').split(/=/, 2)[1] +else + makeprog = 'make' +end + +common_conf = [ + PathItem.new('prefix', 'path', c['prefix'], + 'path prefix of target environment'), + PathItem.new('bindir', 'path', parameterize.call(c['bindir']), + 'the directory for commands'), + PathItem.new('libdir', 'path', libdir, + 'the directory for libraries'), + PathItem.new('datadir', 'path', parameterize.call(c['datadir']), + 'the directory for shared data'), + PathItem.new('mandir', 'path', parameterize.call(c['mandir']), + 'the directory for man pages'), + PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), + 'the directory for man pages'), + PathItem.new('stdruby', 'path', stdruby, + 'the directory for standard ruby libraries'), + PathItem.new('siteruby', 'path', siteruby, + 'the directory for version-independent aux ruby libraries'), + PathItem.new('siterubyver', 'path', siterubyver, + 'the directory for aux ruby libraries'), + PathItem.new('siterubyverarch', 'path', siterubyverarch, + 'the directory for aux ruby binaries'), + PathItem.new('rbdir', 'path', '$siterubyver', + 'the directory for ruby scripts'), + PathItem.new('sodir', 'path', '$siterubyverarch', + 'the directory for ruby extentions'), + PathItem.new('rubypath', 'path', rubypath, + 'the path to set to #! line'), + ProgramItem.new('rubyprog', 'name', rubypath, + 'the ruby program using for installation'), + ProgramItem.new('makeprog', 'name', makeprog, + 'the make program to compile ruby extentions'), + SelectItem.new('shebang', 'all/ruby/never', 'ruby', + 'shebang line (#!) editing mode'), + BoolItem.new('without-ext', 'yes/no', 'no', + 'does not compile/install ruby extentions') +] +class ConfigTable_class # open again + ALIASES = { + 'std-ruby' => 'stdruby', + 'site-ruby-common' => 'siteruby', # For backward compatibility + 'site-ruby' => 'siterubyver', # For backward compatibility + 'bin-dir' => 'bindir', + 'bin-dir' => 'bindir', + 'rb-dir' => 'rbdir', + 'so-dir' => 'sodir', + 'data-dir' => 'datadir', + 'ruby-path' => 'rubypath', + 'ruby-prog' => 'rubyprog', + 'ruby' => 'rubyprog', + 'make-prog' => 'makeprog', + 'make' => 'makeprog' + } +end +multipackage_conf = [ + PackageSelectionItem.new('with', 'name,name...', '', 'ALL', + 'package names that you want to install'), + PackageSelectionItem.new('without', 'name,name...', '', 'NONE', + 'package names that you do not want to install') +] +if multipackage_install? + ConfigTable = ConfigTable_class.new(common_conf + multipackage_conf) +else + ConfigTable = ConfigTable_class.new(common_conf) +end + + +module MetaConfigAPI + + def eval_file_ifexist(fname) + instance_eval File.read(fname), fname, 1 if File.file?(fname) + end + + def config_names + ConfigTable.map {|i| i.name } + end + + def config?(name) + ConfigTable.key?(name) + end + + def bool_config?(name) + ConfigTable.lookup(name).config_type == 'bool' + end + + def path_config?(name) + ConfigTable.lookup(name).config_type == 'path' + end + + def value_config?(name) + case ConfigTable.lookup(name).config_type + when 'bool', 'path' + true + else + false + end + end + + def add_config(item) + ConfigTable.add item + end + + def add_bool_config(name, default, desc) + ConfigTable.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) + end + + def add_path_config(name, default, desc) + ConfigTable.add PathItem.new(name, 'path', default, desc) + end + + def set_config_default(name, default) + ConfigTable.lookup(name).default = default + end + + def remove_config(name) + ConfigTable.remove(name) + end + +end + + +# +# File Operations +# + +module FileOperations + + def mkdir_p(dirname, prefix = nil) + dirname = prefix + File.expand_path(dirname) if prefix + $stderr.puts "mkdir -p #{dirname}" if verbose? + return if no_harm? + + # does not check '/'... it's too abnormal case + dirs = File.expand_path(dirname).split(%r<(?=/)>) + if /\A[a-z]:\z/i =~ dirs[0] + disk = dirs.shift + dirs[0] = disk + dirs[0] + end + dirs.each_index do |idx| + path = dirs[0..idx].join('') + Dir.mkdir path unless File.dir?(path) + end + end + + def rm_f(fname) + $stderr.puts "rm -f #{fname}" if verbose? + return if no_harm? + + if File.exist?(fname) or File.symlink?(fname) + File.chmod 0777, fname + File.unlink fname + end + end + + def rm_rf(dn) + $stderr.puts "rm -rf #{dn}" if verbose? + return if no_harm? + + Dir.chdir dn + Dir.foreach('.') do |fn| + next if fn == '.' + next if fn == '..' + if File.dir?(fn) + verbose_off { + rm_rf fn + } + else + verbose_off { + rm_f fn + } + end + end + Dir.chdir '..' + Dir.rmdir dn + end + + def move_file(src, dest) + File.unlink dest if File.exist?(dest) + begin + File.rename src, dest + rescue + File.open(dest, 'wb') {|f| f.write File.binread(src) } + File.chmod File.stat(src).mode, dest + File.unlink src + end + end + + def install(from, dest, mode, prefix = nil) + $stderr.puts "install #{from} #{dest}" if verbose? + return if no_harm? + + realdest = prefix ? prefix + File.expand_path(dest) : dest + realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) + str = File.binread(from) + if diff?(str, realdest) + verbose_off { + rm_f realdest if File.exist?(realdest) + } + File.open(realdest, 'wb') {|f| + f.write str + } + File.chmod mode, realdest + + File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| + if prefix + f.puts realdest.sub(prefix, '') + else + f.puts realdest + end + } + end + end + + def diff?(new_content, path) + return true unless File.exist?(path) + new_content != File.binread(path) + end + + def command(str) + $stderr.puts str if verbose? + system str or raise RuntimeError, "'system #{str}' failed" + end + + def ruby(str) + command config('rubyprog') + ' ' + str + end + + def make(task = '') + command config('makeprog') + ' ' + task + end + + def extdir?(dir) + File.exist?(dir + '/MANIFEST') + end + + def all_files_in(dirname) + Dir.open(dirname) {|d| + return d.select {|ent| File.file?("#{dirname}/#{ent}") } + } + end + + REJECT_DIRS = %w( + CVS SCCS RCS CVS.adm .svn + ) + + def all_dirs_in(dirname) + Dir.open(dirname) {|d| + return d.select {|n| File.dir?("#{dirname}/#{n}") } - %w(. ..) - REJECT_DIRS + } + end + +end + + +# +# Main Installer +# + +module HookUtils + + def run_hook(name) + try_run_hook "#{curr_srcdir()}/#{name}" or + try_run_hook "#{curr_srcdir()}/#{name}.rb" + end + + def try_run_hook(fname) + return false unless File.file?(fname) + begin + instance_eval File.read(fname), fname, 1 + rescue + setup_rb_error "hook #{fname} failed:\n" + $!.message + end + true + end + +end + + +module HookScriptAPI + + def get_config(key) + @config[key] + end + + alias config get_config + + def set_config(key, val) + @config[key] = val + end + + # + # srcdir/objdir (works only in the package directory) + # + + #abstract srcdir_root + #abstract objdir_root + #abstract relpath + + def curr_srcdir + "#{srcdir_root()}/#{relpath()}" + end + + def curr_objdir + "#{objdir_root()}/#{relpath()}" + end + + def srcfile(path) + "#{curr_srcdir()}/#{path}" + end + + def srcexist?(path) + File.exist?(srcfile(path)) + end + + def srcdirectory?(path) + File.dir?(srcfile(path)) + end + + def srcfile?(path) + File.file? srcfile(path) + end + + def srcentries(path = '.') + Dir.open("#{curr_srcdir()}/#{path}") {|d| + return d.to_a - %w(. ..) + } + end + + def srcfiles(path = '.') + srcentries(path).select {|fname| + File.file?(File.join(curr_srcdir(), path, fname)) + } + end + + def srcdirectories(path = '.') + srcentries(path).select {|fname| + File.dir?(File.join(curr_srcdir(), path, fname)) + } + end + +end + + +class ToplevelInstaller + + Version = '3.3.1' + Copyright = 'Copyright (c) 2000-2004 Minero Aoki' + + TASKS = [ + [ 'all', 'do config, setup, then install' ], + [ 'config', 'saves your configurations' ], + [ 'show', 'shows current configuration' ], + [ 'setup', 'compiles ruby extentions and others' ], + [ 'install', 'installs files' ], + [ 'clean', "does `make clean' for each extention" ], + [ 'distclean',"does `make distclean' for each extention" ] + ] + + def ToplevelInstaller.invoke + instance().invoke + end + + @singleton = nil + + def ToplevelInstaller.instance + @singleton ||= new(File.dirname($0)) + @singleton + end + + include MetaConfigAPI + + def initialize(ardir_root) + @config = nil + @options = { 'verbose' => true } + @ardir = File.expand_path(ardir_root) + end + + def inspect + "#<#{self.class} #{__id__()}>" + end + + def invoke + run_metaconfigs + case task = parsearg_global() + when nil, 'all' + @config = load_config('config') + parsearg_config + init_installers + exec_config + exec_setup + exec_install + else + @config = load_config(task) + __send__ "parsearg_#{task}" + init_installers + __send__ "exec_#{task}" + end + end + + def run_metaconfigs + eval_file_ifexist "#{@ardir}/metaconfig" + end + + def load_config(task) + case task + when 'config' + ConfigTable.new + when 'clean', 'distclean' + if File.exist?(ConfigTable.savefile) + then ConfigTable.load + else ConfigTable.new + end + else + ConfigTable.load + end + end + + def init_installers + @installer = Installer.new(@config, @options, @ardir, File.expand_path('.')) + end + + # + # Hook Script API bases + # + + def srcdir_root + @ardir + end + + def objdir_root + '.' + end + + def relpath + '.' + end + + # + # Option Parsing + # + + def parsearg_global + valid_task = /\A(?:#{TASKS.map {|task,desc| task }.join '|'})\z/ + + while arg = ARGV.shift + case arg + when /\A\w+\z/ + setup_rb_error "invalid task: #{arg}" unless valid_task =~ arg + return arg + + when '-q', '--quiet' + @options['verbose'] = false + + when '--verbose' + @options['verbose'] = true + + when '-h', '--help' + print_usage $stdout + exit 0 + + when '-v', '--version' + puts "#{File.basename($0)} version #{Version}" + exit 0 + + when '--copyright' + puts Copyright + exit 0 + + else + setup_rb_error "unknown global option '#{arg}'" + end + end + + nil + end + + + def parsearg_no_options + unless ARGV.empty? + setup_rb_error "#{task}: unknown options: #{ARGV.join ' '}" + end + end + + alias parsearg_show parsearg_no_options + alias parsearg_setup parsearg_no_options + alias parsearg_clean parsearg_no_options + alias parsearg_distclean parsearg_no_options + + def parsearg_config + re = /\A--(#{ConfigTable.map {|i| i.name }.join('|')})(?:=(.*))?\z/ + @options['config-opt'] = [] + + while i = ARGV.shift + if /\A--?\z/ =~ i + @options['config-opt'] = ARGV.dup + break + end + m = re.match(i) or setup_rb_error "config: unknown option #{i}" + name, value = *m.to_a[1,2] + @config[name] = value + end + end + + def parsearg_install + @options['no-harm'] = false + @options['install-prefix'] = '' + while a = ARGV.shift + case a + when /\A--no-harm\z/ + @options['no-harm'] = true + when /\A--prefix=(.*)\z/ + path = $1 + path = File.expand_path(path) unless path[0,1] == '/' + @options['install-prefix'] = path + else + setup_rb_error "install: unknown option #{a}" + end + end + end + + def print_usage(out) + out.puts 'Typical Installation Procedure:' + out.puts " $ ruby #{File.basename $0} config" + out.puts " $ ruby #{File.basename $0} setup" + out.puts " # ruby #{File.basename $0} install (may require root privilege)" + out.puts + out.puts 'Detailed Usage:' + out.puts " ruby #{File.basename $0} " + out.puts " ruby #{File.basename $0} [] []" + + fmt = " %-24s %s\n" + out.puts + out.puts 'Global options:' + out.printf fmt, '-q,--quiet', 'suppress message outputs' + out.printf fmt, ' --verbose', 'output messages verbosely' + out.printf fmt, '-h,--help', 'print this message' + out.printf fmt, '-v,--version', 'print version and quit' + out.printf fmt, ' --copyright', 'print copyright and quit' + out.puts + out.puts 'Tasks:' + TASKS.each do |name, desc| + out.printf fmt, name, desc + end + + fmt = " %-24s %s [%s]\n" + out.puts + out.puts 'Options for CONFIG or ALL:' + ConfigTable.each do |item| + out.printf fmt, item.help_opt, item.description, item.help_default + end + out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" + out.puts + out.puts 'Options for INSTALL:' + out.printf fmt, '--no-harm', 'only display what to do if given', 'off' + out.printf fmt, '--prefix=path', 'install path prefix', '$prefix' + out.puts + end + + # + # Task Handlers + # + + def exec_config + @installer.exec_config + @config.save # must be final + end + + def exec_setup + @installer.exec_setup + end + + def exec_install + @installer.exec_install + end + + def exec_show + ConfigTable.each do |i| + printf "%-20s %s\n", i.name, i.value + end + end + + def exec_clean + @installer.exec_clean + end + + def exec_distclean + @installer.exec_distclean + end + +end + + +class ToplevelInstallerMulti < ToplevelInstaller + + include HookUtils + include HookScriptAPI + include FileOperations + + def initialize(ardir) + super + @packages = all_dirs_in("#{@ardir}/packages") + raise 'no package exists' if @packages.empty? + end + + def run_metaconfigs + eval_file_ifexist "#{@ardir}/metaconfig" + @packages.each do |name| + eval_file_ifexist "#{@ardir}/packages/#{name}/metaconfig" + end + end + + def init_installers + @installers = {} + @packages.each do |pack| + @installers[pack] = Installer.new(@config, @options, + "#{@ardir}/packages/#{pack}", + "packages/#{pack}") + end + + with = extract_selection(config('with')) + without = extract_selection(config('without')) + @selected = @installers.keys.select {|name| + (with.empty? or with.include?(name)) \ + and not without.include?(name) + } + end + + def extract_selection(list) + a = list.split(/,/) + a.each do |name| + setup_rb_error "no such package: #{name}" unless @installers.key?(name) + end + a + end + + def print_usage(f) + super + f.puts 'Inluded packages:' + f.puts ' ' + @packages.sort.join(' ') + f.puts + end + + # + # multi-package metaconfig API + # + + attr_reader :packages + + def declare_packages(list) + raise 'package list is empty' if list.empty? + list.each do |name| + raise "directory packages/#{name} does not exist"\ + unless File.dir?("#{@ardir}/packages/#{name}") + end + @packages = list + end + + # + # Task Handlers + # + + def exec_config + run_hook 'pre-config' + each_selected_installers {|inst| inst.exec_config } + run_hook 'post-config' + @config.save # must be final + end + + def exec_setup + run_hook 'pre-setup' + each_selected_installers {|inst| inst.exec_setup } + run_hook 'post-setup' + end + + def exec_install + run_hook 'pre-install' + each_selected_installers {|inst| inst.exec_install } + run_hook 'post-install' + end + + def exec_clean + rm_f ConfigTable.savefile + run_hook 'pre-clean' + each_selected_installers {|inst| inst.exec_clean } + run_hook 'post-clean' + end + + def exec_distclean + rm_f ConfigTable.savefile + run_hook 'pre-distclean' + each_selected_installers {|inst| inst.exec_distclean } + run_hook 'post-distclean' + end + + # + # lib + # + + def each_selected_installers + Dir.mkdir 'packages' unless File.dir?('packages') + @selected.each do |pack| + $stderr.puts "Processing the package `#{pack}' ..." if @options['verbose'] + Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") + Dir.chdir "packages/#{pack}" + yield @installers[pack] + Dir.chdir '../..' + end + end + + def verbose? + @options['verbose'] + end + + def no_harm? + @options['no-harm'] + end + +end + + +class Installer + + FILETYPES = %w( bin lib ext data ) + + include HookScriptAPI + include HookUtils + include FileOperations + + def initialize(config, opt, srcroot, objroot) + @config = config + @options = opt + @srcdir = File.expand_path(srcroot) + @objdir = File.expand_path(objroot) + @currdir = '.' + end + + def inspect + "#<#{self.class} #{File.basename(@srcdir)}>" + end + + # + # Hook Script API base methods + # + + def srcdir_root + @srcdir + end + + def objdir_root + @objdir + end + + def relpath + @currdir + end + + # + # configs/options + # + + def no_harm? + @options['no-harm'] + end + + def verbose? + @options['verbose'] + end + + def verbose_off + begin + save, @options['verbose'] = @options['verbose'], false + yield + ensure + @options['verbose'] = save + end + end + + # + # TASK config + # + + def exec_config + exec_task_traverse 'config' + end + + def config_dir_bin(rel) + end + + def config_dir_lib(rel) + end + + def config_dir_ext(rel) + extconf if extdir?(curr_srcdir()) + end + + def extconf + opt = @options['config-opt'].join(' ') + command "#{config('rubyprog')} #{curr_srcdir()}/extconf.rb #{opt}" + end + + def config_dir_data(rel) + end + + # + # TASK setup + # + + def exec_setup + exec_task_traverse 'setup' + end + + def setup_dir_bin(rel) + all_files_in(curr_srcdir()).each do |fname| + adjust_shebang "#{curr_srcdir()}/#{fname}" + end + end + + def adjust_shebang(path) + return if no_harm? + tmpfile = File.basename(path) + '.tmp' + begin + File.open(path, 'rb') {|r| + first = r.gets + return unless File.basename(config('rubypath')) == 'ruby' + return unless File.basename(first.sub(/\A\#!/, '').split[0]) == 'ruby' + $stderr.puts "adjusting shebang: #{File.basename(path)}" if verbose? + File.open(tmpfile, 'wb') {|w| + w.print first.sub(/\A\#!\s*\S+/, '#! ' + config('rubypath')) + w.write r.read + } + move_file tmpfile, File.basename(path) + } + ensure + File.unlink tmpfile if File.exist?(tmpfile) + end + end + + def setup_dir_lib(rel) + end + + def setup_dir_ext(rel) + make if extdir?(curr_srcdir()) + end + + def setup_dir_data(rel) + end + + # + # TASK install + # + + def exec_install + rm_f 'InstalledFiles' + exec_task_traverse 'install' + end + + def install_dir_bin(rel) + install_files collect_filenames_auto(), "#{config('bindir')}/#{rel}", 0755 + end + + def install_dir_lib(rel) + install_files ruby_scripts(), "#{config('rbdir')}/#{rel}", 0644 + end + + def install_dir_ext(rel) + return unless extdir?(curr_srcdir()) + install_files ruby_extentions('.'), + "#{config('sodir')}/#{File.dirname(rel)}", + 0555 + end + + def install_dir_data(rel) + install_files collect_filenames_auto(), "#{config('datadir')}/#{rel}", 0644 + end + + def install_files(list, dest, mode) + mkdir_p dest, @options['install-prefix'] + list.each do |fname| + install fname, dest, mode, @options['install-prefix'] + end + end + + def ruby_scripts + collect_filenames_auto().select {|n| /\.rb\z/ =~ n } + end + + # picked up many entries from cvs-1.11.1/src/ignore.c + reject_patterns = %w( + core RCSLOG tags TAGS .make.state + .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb + *~ *.old *.bak *.BAK *.orig *.rej _$* *$ + + *.org *.in .* + ) + mapping = { + '.' => '\.', + '$' => '\$', + '#' => '\#', + '*' => '.*' + } + REJECT_PATTERNS = Regexp.new('\A(?:' + + reject_patterns.map {|pat| + pat.gsub(/[\.\$\#\*]/) {|ch| mapping[ch] } + }.join('|') + + ')\z') + + def collect_filenames_auto + mapdir((existfiles() - hookfiles()).reject {|fname| + REJECT_PATTERNS =~ fname + }) + end + + def existfiles + all_files_in(curr_srcdir()) | all_files_in('.') + end + + def hookfiles + %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| + %w( config setup install clean ).map {|t| sprintf(fmt, t) } + }.flatten + end + + def mapdir(filelist) + filelist.map {|fname| + if File.exist?(fname) # objdir + fname + else # srcdir + File.join(curr_srcdir(), fname) + end + } + end + + def ruby_extentions(dir) + Dir.open(dir) {|d| + ents = d.select {|fname| /\.#{::Config::CONFIG['DLEXT']}\z/ =~ fname } + if ents.empty? + setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" + end + return ents + } + end + + # + # TASK clean + # + + def exec_clean + exec_task_traverse 'clean' + rm_f ConfigTable.savefile + rm_f 'InstalledFiles' + end + + def clean_dir_bin(rel) + end + + def clean_dir_lib(rel) + end + + def clean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'clean' if File.file?('Makefile') + end + + def clean_dir_data(rel) + end + + # + # TASK distclean + # + + def exec_distclean + exec_task_traverse 'distclean' + rm_f ConfigTable.savefile + rm_f 'InstalledFiles' + end + + def distclean_dir_bin(rel) + end + + def distclean_dir_lib(rel) + end + + def distclean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'distclean' if File.file?('Makefile') + end + + # + # lib + # + + def exec_task_traverse(task) + run_hook "pre-#{task}" + FILETYPES.each do |type| + if config('without-ext') == 'yes' and type == 'ext' + $stderr.puts 'skipping ext/* by user option' if verbose? + next + end + traverse task, type, "#{task}_dir_#{type}" + end + run_hook "post-#{task}" + end + + def traverse(task, rel, mid) + dive_into(rel) { + run_hook "pre-#{task}" + __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') + all_dirs_in(curr_srcdir()).each do |d| + traverse task, "#{rel}/#{d}", mid + end + run_hook "post-#{task}" + } + end + + def dive_into(rel) + return unless File.dir?("#{@srcdir}/#{rel}") + + dir = File.basename(rel) + Dir.mkdir dir unless File.dir?(dir) + prevdir = Dir.pwd + Dir.chdir dir + $stderr.puts '---> ' + rel if verbose? + @currdir = rel + yield + Dir.chdir prevdir + $stderr.puts '<--- ' + rel if verbose? + @currdir = File.dirname(rel) + end + +end + + +if $0 == __FILE__ + begin + if multipackage_install? + ToplevelInstallerMulti.invoke + else + ToplevelInstaller.invoke + end + rescue SetupError + raise if $DEBUG + $stderr.puts $!.message + $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." + exit 1 + end +end diff --git a/vendor/rails/actionwebservice/test/abstract_client.rb b/vendor/rails/actionwebservice/test/abstract_client.rb new file mode 100644 index 00000000..5207d8ef --- /dev/null +++ b/vendor/rails/actionwebservice/test/abstract_client.rb @@ -0,0 +1,184 @@ +require File.dirname(__FILE__) + '/abstract_unit' +require 'webrick' +require 'webrick/log' +require 'singleton' + +module ClientTest + class Person < ActionWebService::Struct + member :firstnames, [:string] + member :lastname, :string + + def ==(other) + firstnames == other.firstnames && lastname == other.lastname + end + end + + class Inner < ActionWebService::Struct + member :name, :string + end + + class Outer < ActionWebService::Struct + member :name, :string + member :inner, Inner + end + + class User < ActiveRecord::Base + end + + module Accounting + class User < ActiveRecord::Base + end + end + + class WithModel < ActionWebService::Struct + member :user, User + member :users, [User] + end + + class WithMultiDimArray < ActionWebService::Struct + member :pref, [[:string]] + end + + class API < ActionWebService::API::Base + api_method :void + api_method :normal, :expects => [:int, :int], :returns => [:int] + api_method :array_return, :returns => [[Person]] + api_method :struct_pass, :expects => [[Person]], :returns => [:bool] + api_method :nil_struct_return, :returns => [Person] + api_method :inner_nil, :returns => [Outer] + api_method :client_container, :returns => [:int] + api_method :named_parameters, :expects => [{:key=>:string}, {:id=>:int}] + api_method :thrower + api_method :user_return, :returns => [User] + api_method :with_model_return, :returns => [WithModel] + api_method :scoped_model_return, :returns => [Accounting::User] + api_method :multi_dim_return, :returns => [WithMultiDimArray] + end + + class NullLogOut + def <<(*args); end + end + + class Container < ActionController::Base + web_service_api API + + attr_accessor :value_void + attr_accessor :value_normal + attr_accessor :value_array_return + attr_accessor :value_struct_pass + attr_accessor :value_named_parameters + + def initialize + @session = @assigns = {} + @value_void = nil + @value_normal = nil + @value_array_return = nil + @value_struct_pass = nil + @value_named_parameters = nil + end + + def void + @value_void = @method_params + end + + def normal + @value_normal = @method_params + 5 + end + + def array_return + person = Person.new + person.firstnames = ["one", "two"] + person.lastname = "last" + @value_array_return = [person] + end + + def struct_pass + @value_struct_pass = @method_params + true + end + + def nil_struct_return + nil + end + + def inner_nil + Outer.new :name => 'outer', :inner => nil + end + + def client_container + 50 + end + + def named_parameters + @value_named_parameters = @method_params + end + + def thrower + raise "Hi" + end + + def user_return + User.find(1) + end + + def with_model_return + WithModel.new :user => User.find(1), :users => User.find(:all) + end + + def scoped_model_return + Accounting::User.find(1) + end + + def multi_dim_return + WithMultiDimArray.new :pref => [%w{pref1 value1}, %w{pref2 value2}] + end + end + + class AbstractClientLet < WEBrick::HTTPServlet::AbstractServlet + def initialize(controller) + @controller = controller + end + + def get_instance(*args) + self + end + + def require_path_info? + false + end + + def do_GET(req, res) + raise WEBrick::HTTPStatus::MethodNotAllowed, "GET request not allowed." + end + + def do_POST(req, res) + raise NotImplementedError + end + end + + class AbstractServer + include ClientTest + include Singleton + attr :container + def initialize + @container = Container.new + @clientlet = create_clientlet(@container) + log = WEBrick::BasicLog.new(NullLogOut.new) + @server = WEBrick::HTTPServer.new(:Port => server_port, :Logger => log, :AccessLog => log) + @server.mount('/', @clientlet) + @thr = Thread.new { @server.start } + until @server.status == :Running; end + at_exit { @server.stop; @thr.join } + end + + protected + def create_clientlet + raise NotImplementedError + end + + def server_port + raise NotImplementedError + end + end +end diff --git a/vendor/rails/actionwebservice/test/abstract_dispatcher.rb b/vendor/rails/actionwebservice/test/abstract_dispatcher.rb new file mode 100644 index 00000000..b857e4a9 --- /dev/null +++ b/vendor/rails/actionwebservice/test/abstract_dispatcher.rb @@ -0,0 +1,500 @@ +require File.dirname(__FILE__) + '/abstract_unit' +require 'stringio' + +class ActionController::Base; def rescue_action(e) raise e end; end + +module DispatcherTest + Utf8String = "One World Caf\303\251" + WsdlNamespace = 'http://rubyonrails.com/some/namespace' + + class Node < ActiveRecord::Base + def initialize(*args) + super(*args) + @new_record = false + end + + class << self + def name + "DispatcherTest::Node" + end + + def columns(*args) + [ + ActiveRecord::ConnectionAdapters::Column.new('id', 0, 'int'), + ActiveRecord::ConnectionAdapters::Column.new('name', nil, 'string'), + ActiveRecord::ConnectionAdapters::Column.new('description', nil, 'string'), + ] + end + + def connection + self + end + end + end + + class Person < ActionWebService::Struct + member :id, :int + member :name, :string + + def ==(other) + self.id == other.id && self.name == other.name + end + end + + class API < ActionWebService::API::Base + api_method :add, :expects => [:int, :int], :returns => [:int] + api_method :interceptee + api_method :struct_return, :returns => [[Node]] + api_method :void + end + + class DirectAPI < ActionWebService::API::Base + api_method :add, :expects => [{:a=>:int}, {:b=>:int}], :returns => [:int] + api_method :add2, :expects => [{:a=>:int}, {:b=>:int}], :returns => [:int] + api_method :before_filtered + api_method :after_filtered, :returns => [[:int]] + api_method :struct_return, :returns => [[Node]] + api_method :struct_pass, :expects => [{:person => Person}] + api_method :base_struct_return, :returns => [[Person]] + api_method :hash_struct_return, :returns => [[Person]] + api_method :thrower + api_method :void + api_method :test_utf8, :returns => [:string] + api_method :hex, :expects => [:base64], :returns => [:string] + api_method :unhex, :expects => [:string], :returns => [:base64] + api_method :time, :expects => [:time], :returns => [:time] + end + + class VirtualAPI < ActionWebService::API::Base + default_api_method :fallback + end + + class Service < ActionWebService::Base + web_service_api API + + before_invocation :do_intercept, :only => [:interceptee] + + attr :added + attr :intercepted + attr :void_called + + def initialize + @void_called = false + end + + def add(a, b) + @added = a + b + end + + def interceptee + @intercepted = false + end + + def struct_return + n1 = Node.new('id' => 1, 'name' => 'node1', 'description' => 'Node 1') + n2 = Node.new('id' => 2, 'name' => 'node2', 'description' => 'Node 2') + [n1, n2] + end + + def void(*args) + @void_called = args + end + + def do_intercept(name, args) + [false, "permission denied"] + end + end + + class MTAPI < ActionWebService::API::Base + inflect_names false + api_method :getCategories, :returns => [[:string]] + api_method :bool, :returns => [:bool] + api_method :alwaysFail + end + + class BloggerAPI < ActionWebService::API::Base + inflect_names false + api_method :getCategories, :returns => [[:string]] + api_method :str, :expects => [:int], :returns => [:string] + api_method :alwaysFail + end + + class MTService < ActionWebService::Base + web_service_api MTAPI + + def getCategories + ["mtCat1", "mtCat2"] + end + + def bool + 'y' + end + + def alwaysFail + raise "MT AlwaysFail" + end + end + + class BloggerService < ActionWebService::Base + web_service_api BloggerAPI + + def getCategories + ["bloggerCat1", "bloggerCat2"] + end + + def str(int) + unless int.is_a?(Integer) + raise "Not an integer!" + end + 500 + int + end + + def alwaysFail + raise "Blogger AlwaysFail" + end + end + + class AbstractController < ActionController::Base + def generate_wsdl + @request ||= ::ActionController::TestRequest.new + to_wsdl + end + end + + class DelegatedController < AbstractController + web_service_dispatching_mode :delegated + wsdl_namespace WsdlNamespace + + web_service(:test_service) { @service ||= Service.new; @service } + end + + class LayeredController < AbstractController + web_service_dispatching_mode :layered + wsdl_namespace WsdlNamespace + + web_service(:mt) { @mt_service ||= MTService.new; @mt_service } + web_service(:blogger) { @blogger_service ||= BloggerService.new; @blogger_service } + end + + class DirectController < AbstractController + web_service_api DirectAPI + web_service_dispatching_mode :direct + wsdl_namespace WsdlNamespace + + before_invocation :alwaysfail, :only => [:before_filtered] + after_invocation :alwaysok, :only => [:after_filtered] + + attr :added + attr :added2 + attr :before_filter_called + attr :before_filter_target_called + attr :after_filter_called + attr :after_filter_target_called + attr :void_called + attr :struct_pass_value + + def initialize + @before_filter_called = false + @before_filter_target_called = false + @after_filter_called = false + @after_filter_target_called = false + @void_called = false + @struct_pass_value = false + end + + def add + @added = @params['a'] + @params['b'] + end + + def add2(a, b) + @added2 = a + b + end + + def before_filtered + @before_filter_target_called = true + end + + def after_filtered + @after_filter_target_called = true + [5, 6, 7] + end + + def thrower + raise "Hi, I'm an exception" + end + + def struct_return + n1 = Node.new('id' => 1, 'name' => 'node1', 'description' => 'Node 1') + n2 = Node.new('id' => 2, 'name' => 'node2', 'description' => 'Node 2') + [n1, n2] + end + + def struct_pass(person) + @struct_pass_value = person + end + + def base_struct_return + p1 = Person.new('id' => 1, 'name' => 'person1') + p2 = Person.new('id' => 2, 'name' => 'person2') + [p1, p2] + end + + def hash_struct_return + p1 = { :id => '1', 'name' => 'test' } + p2 = { 'id' => '2', :name => 'person2' } + [p1, p2] + end + + def void + @void_called = @method_params + end + + def test_utf8 + Utf8String + end + + def hex(s) + return s.unpack("H*")[0] + end + + def unhex(s) + return [s].pack("H*") + end + + def time(t) + t + end + + protected + def alwaysfail(method_name, params) + @before_filter_called = true + false + end + + def alwaysok(method_name, params, return_value) + @after_filter_called = true + end + end + + class VirtualController < AbstractController + web_service_api VirtualAPI + wsdl_namespace WsdlNamespace + + def fallback + "fallback!" + end + end +end + +module DispatcherCommonTests + def test_direct_dispatching + assert_equal(70, do_method_call(@direct_controller, 'Add', 20, 50)) + assert_equal(70, @direct_controller.added) + assert_equal(50, do_method_call(@direct_controller, 'Add2', 25, 25)) + assert_equal(50, @direct_controller.added2) + assert(@direct_controller.void_called == false) + assert(do_method_call(@direct_controller, 'Void', 3, 4, 5).nil?) + assert(@direct_controller.void_called == []) + result = do_method_call(@direct_controller, 'BaseStructReturn') + assert(result[0].is_a?(DispatcherTest::Person)) + assert(result[1].is_a?(DispatcherTest::Person)) + assert_equal("cafe", do_method_call(@direct_controller, 'Hex', "\xca\xfe")) + assert_equal("\xca\xfe", do_method_call(@direct_controller, 'Unhex', "cafe")) + time = Time.gm(1998, "Feb", 02, 15, 12, 01) + assert_equal(time, do_method_call(@direct_controller, 'Time', time)) + end + + def test_direct_entrypoint + assert(@direct_controller.respond_to?(:api)) + end + + def test_virtual_dispatching + assert_equal("fallback!", do_method_call(@virtual_controller, 'VirtualOne')) + assert_equal("fallback!", do_method_call(@virtual_controller, 'VirtualTwo')) + end + + def test_direct_filtering + assert_equal(false, @direct_controller.before_filter_called) + assert_equal(false, @direct_controller.before_filter_target_called) + do_method_call(@direct_controller, 'BeforeFiltered') + assert_equal(true, @direct_controller.before_filter_called) + assert_equal(false, @direct_controller.before_filter_target_called) + assert_equal(false, @direct_controller.after_filter_called) + assert_equal(false, @direct_controller.after_filter_target_called) + assert_equal([5, 6, 7], do_method_call(@direct_controller, 'AfterFiltered')) + assert_equal(true, @direct_controller.after_filter_called) + assert_equal(true, @direct_controller.after_filter_target_called) + end + + def test_delegated_dispatching + assert_equal(130, do_method_call(@delegated_controller, 'Add', 50, 80)) + service = @delegated_controller.web_service_object(:test_service) + assert_equal(130, service.added) + @delegated_controller.web_service_exception_reporting = true + assert(service.intercepted.nil?) + result = do_method_call(@delegated_controller, 'Interceptee') + assert(service.intercepted.nil?) + assert(is_exception?(result)) + assert_match(/permission denied/, exception_message(result)) + result = do_method_call(@delegated_controller, 'NonExistentMethod') + assert(is_exception?(result)) + assert_match(/NonExistentMethod/, exception_message(result)) + assert(service.void_called == false) + assert(do_method_call(@delegated_controller, 'Void', 3, 4, 5).nil?) + assert(service.void_called == []) + end + + def test_garbage_request + [@direct_controller, @delegated_controller].each do |controller| + controller.class.web_service_exception_reporting = true + send_garbage_request = lambda do + service_name = service_name(controller) + request = protocol.encode_action_pack_request(service_name, 'broken, method, name!', 'broken request body', :request_class => ActionController::TestRequest) + response = ActionController::TestResponse.new + controller.process(request, response) + # puts response.body + assert(response.headers['Status'] =~ /^500/) + end + send_garbage_request.call + controller.class.web_service_exception_reporting = false + send_garbage_request.call + end + end + + def test_exception_marshaling + @direct_controller.web_service_exception_reporting = true + result = do_method_call(@direct_controller, 'Thrower') + assert(is_exception?(result)) + assert_equal("Hi, I'm an exception", exception_message(result)) + @direct_controller.web_service_exception_reporting = false + result = do_method_call(@direct_controller, 'Thrower') + assert(exception_message(result) != "Hi, I'm an exception") + end + + def test_ar_struct_return + [@direct_controller, @delegated_controller].each do |controller| + result = do_method_call(controller, 'StructReturn') + assert(result[0].is_a?(DispatcherTest::Node)) + assert(result[1].is_a?(DispatcherTest::Node)) + assert_equal('node1', result[0].name) + assert_equal('node2', result[1].name) + end + end + + def test_casting + assert_equal 70, do_method_call(@direct_controller, 'Add', "50", "20") + assert_equal false, @direct_controller.struct_pass_value + person = DispatcherTest::Person.new(:id => 1, :name => 'test') + result = do_method_call(@direct_controller, 'StructPass', person) + assert(nil == result || true == result) + assert_equal person, @direct_controller.struct_pass_value + assert !person.equal?(@direct_controller.struct_pass_value) + result = do_method_call(@direct_controller, 'StructPass', {'id' => '1', 'name' => 'test'}) + case + when soap? + assert_equal(person, @direct_controller.struct_pass_value) + assert !person.equal?(@direct_controller.struct_pass_value) + when xmlrpc? + assert_equal(person, @direct_controller.struct_pass_value) + assert !person.equal?(@direct_controller.struct_pass_value) + end + assert_equal person, do_method_call(@direct_controller, 'HashStructReturn')[0] + result = do_method_call(@direct_controller, 'StructPass', {'id' => '1', 'name' => 'test', 'nonexistent_attribute' => 'value'}) + case + when soap? + assert_equal(person, @direct_controller.struct_pass_value) + assert !person.equal?(@direct_controller.struct_pass_value) + when xmlrpc? + assert_equal(person, @direct_controller.struct_pass_value) + assert !person.equal?(@direct_controller.struct_pass_value) + end + end + + def test_logging + buf = "" + ActionController::Base.logger = Logger.new(StringIO.new(buf)) + test_casting + test_garbage_request + test_exception_marshaling + ActionController::Base.logger = nil + assert_match /Web Service Response/, buf + assert_match /Web Service Request/, buf + end + + protected + def service_name(container) + raise NotImplementedError + end + + def exception_message(obj) + raise NotImplementedError + end + + def is_exception?(obj) + raise NotImplementedError + end + + def protocol + @protocol + end + + def soap? + protocol.is_a? ActionWebService::Protocol::Soap::SoapProtocol + end + + def xmlrpc? + protocol.is_a? ActionWebService::Protocol::XmlRpc::XmlRpcProtocol + end + + def do_method_call(container, public_method_name, *params) + request_env = {} + mode = container.web_service_dispatching_mode + case mode + when :direct + service_name = service_name(container) + api = container.class.web_service_api + method = api.public_api_method_instance(public_method_name) + when :delegated + service_name = service_name(container) + api = container.web_service_object(service_name).class.web_service_api + method = api.public_api_method_instance(public_method_name) + when :layered + service_name = nil + real_method_name = nil + if public_method_name =~ /^([^\.]+)\.(.*)$/ + service_name = $1 + real_method_name = $2 + end + if soap? + public_method_name = real_method_name + request_env['HTTP_SOAPACTION'] = "/soap/#{service_name}/#{real_method_name}" + end + api = container.web_service_object(service_name.to_sym).class.web_service_api rescue nil + method = api.public_api_method_instance(real_method_name) rescue nil + service_name = self.service_name(container) + end + protocol.register_api(api) + virtual = false + unless method + virtual = true + method ||= ActionWebService::API::Method.new(public_method_name.underscore.to_sym, public_method_name, nil, nil) + end + body = protocol.encode_request(public_method_name, params.dup, method.expects) + # puts body + ap_request = protocol.encode_action_pack_request(service_name, public_method_name, body, :request_class => ActionController::TestRequest) + ap_request.env.update(request_env) + ap_response = ActionController::TestResponse.new + container.process(ap_request, ap_response) + # puts ap_response.body + @response_body = ap_response.body + public_method_name, return_value = protocol.decode_response(ap_response.body) + unless is_exception?(return_value) || virtual + return_value = method.cast_returns(return_value) + end + if soap? + # http://dev.rubyonrails.com/changeset/920 + assert_match(/Response$/, public_method_name) unless public_method_name == "fault" + end + return_value + end +end diff --git a/vendor/rails/actionwebservice/test/abstract_unit.rb b/vendor/rails/actionwebservice/test/abstract_unit.rb new file mode 100644 index 00000000..6862d222 --- /dev/null +++ b/vendor/rails/actionwebservice/test/abstract_unit.rb @@ -0,0 +1,38 @@ +ENV["RAILS_ENV"] = "test" +$:.unshift(File.dirname(__FILE__) + '/../lib') +$:.unshift(File.dirname(__FILE__) + '/../../activesupport/lib') +$:.unshift(File.dirname(__FILE__) + '/../../actionpack/lib') +$:.unshift(File.dirname(__FILE__) + '/../../activerecord/lib') + +require 'test/unit' +require 'action_web_service' +require 'action_controller' +require 'action_controller/test_process' + +ActionController::Base.logger = nil +ActionController::Base.ignore_missing_templates = true + +begin + PATH_TO_AR = File.dirname(__FILE__) + '/../../activerecord' + require "#{PATH_TO_AR}/lib/active_record" unless Object.const_defined?(:ActiveRecord) + require "#{PATH_TO_AR}/lib/active_record/fixtures" unless Object.const_defined?(:Fixtures) +rescue Object => e + fail "\nFailed to load activerecord: #{e}" +end + +ActiveRecord::Base.establish_connection( + :adapter => "mysql", + :username => "rails", + :encoding => "utf8", + :database => "activewebservice_unittest" +) +ActiveRecord::Base.connection + +Test::Unit::TestCase.fixture_path = "#{File.dirname(__FILE__)}/fixtures/" + +# restore default raw_post functionality +class ActionController::TestRequest + def raw_post + super + end +end \ No newline at end of file diff --git a/vendor/rails/actionwebservice/test/api_test.rb b/vendor/rails/actionwebservice/test/api_test.rb new file mode 100644 index 00000000..0e58d848 --- /dev/null +++ b/vendor/rails/actionwebservice/test/api_test.rb @@ -0,0 +1,102 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +module APITest + class API < ActionWebService::API::Base + api_method :void + api_method :expects_and_returns, :expects_and_returns => [:string] + api_method :expects, :expects => [:int, :bool] + api_method :returns, :returns => [:int, [:string]] + api_method :named_signature, :expects => [{:appkey=>:int}, {:publish=>:bool}] + api_method :string_types, :expects => ['int', 'string', 'bool', 'base64'] + api_method :class_types, :expects => [TrueClass, Bignum, String] + end +end + +class TC_API < Test::Unit::TestCase + API = APITest::API + + def test_api_method_declaration + %w( + void + expects_and_returns + expects + returns + named_signature + string_types + class_types + ).each do |name| + name = name.to_sym + public_name = API.public_api_method_name(name) + assert(API.has_api_method?(name)) + assert(API.has_public_api_method?(public_name)) + assert(API.api_method_name(public_name) == name) + assert(API.api_methods.has_key?(name)) + end + end + + def test_signature_canonicalization + assert_equal(nil, API.api_methods[:void].expects) + assert_equal(nil, API.api_methods[:void].returns) + assert_equal([String], API.api_methods[:expects_and_returns].expects.map{|x| x.type_class}) + assert_equal([String], API.api_methods[:expects_and_returns].returns.map{|x| x.type_class}) + assert_equal([Integer, TrueClass], API.api_methods[:expects].expects.map{|x| x.type_class}) + assert_equal(nil, API.api_methods[:expects].returns) + assert_equal(nil, API.api_methods[:returns].expects) + assert_equal([Integer, [String]], API.api_methods[:returns].returns.map{|x| x.array?? [x.element_type.type_class] : x.type_class}) + assert_equal([[:appkey, Integer], [:publish, TrueClass]], API.api_methods[:named_signature].expects.map{|x| [x.name, x.type_class]}) + assert_equal(nil, API.api_methods[:named_signature].returns) + assert_equal([Integer, String, TrueClass, ActionWebService::Base64], API.api_methods[:string_types].expects.map{|x| x.type_class}) + assert_equal(nil, API.api_methods[:string_types].returns) + assert_equal([TrueClass, Integer, String], API.api_methods[:class_types].expects.map{|x| x.type_class}) + assert_equal(nil, API.api_methods[:class_types].returns) + end + + def test_not_instantiable + assert_raises(NoMethodError) do + API.new + end + end + + def test_api_errors + assert_raises(ActionWebService::ActionWebServiceError) do + klass = Class.new(ActionWebService::API::Base) do + api_method :test, :expects => [ActiveRecord::Base] + end + end + klass = Class.new(ActionWebService::API::Base) do + allow_active_record_expects true + api_method :test2, :expects => [ActiveRecord::Base] + end + assert_raises(ActionWebService::ActionWebServiceError) do + klass = Class.new(ActionWebService::API::Base) do + api_method :test, :invalid => [:int] + end + end + end + + def test_parameter_names + method = API.api_methods[:named_signature] + assert_equal 0, method.expects_index_of(:appkey) + assert_equal 1, method.expects_index_of(:publish) + assert_equal 1, method.expects_index_of('publish') + assert_equal 0, method.expects_index_of('appkey') + assert_equal -1, method.expects_index_of('blah') + assert_equal -1, method.expects_index_of(:missing) + assert_equal -1, API.api_methods[:void].expects_index_of('test') + end + + def test_parameter_hash + method = API.api_methods[:named_signature] + hash = method.expects_to_hash([5, false]) + assert_equal({:appkey => 5, :publish => false}, hash) + end + + def test_api_methods_compat + sig = API.api_methods[:named_signature][:expects] + assert_equal [{:appkey=>Integer}, {:publish=>TrueClass}], sig + end + + def test_to_s + assert_equal 'void Expects(int param0, bool param1)', APITest::API.api_methods[:expects].to_s + end +end diff --git a/vendor/rails/actionwebservice/test/apis/auto_load_api.rb b/vendor/rails/actionwebservice/test/apis/auto_load_api.rb new file mode 100644 index 00000000..a35bbe3f --- /dev/null +++ b/vendor/rails/actionwebservice/test/apis/auto_load_api.rb @@ -0,0 +1,3 @@ +class AutoLoadAPI < ActionWebService::API::Base + api_method :void +end diff --git a/vendor/rails/actionwebservice/test/apis/broken_auto_load_api.rb b/vendor/rails/actionwebservice/test/apis/broken_auto_load_api.rb new file mode 100644 index 00000000..139597f9 --- /dev/null +++ b/vendor/rails/actionwebservice/test/apis/broken_auto_load_api.rb @@ -0,0 +1,2 @@ + + diff --git a/vendor/rails/actionwebservice/test/base_test.rb b/vendor/rails/actionwebservice/test/base_test.rb new file mode 100644 index 00000000..55a112a0 --- /dev/null +++ b/vendor/rails/actionwebservice/test/base_test.rb @@ -0,0 +1,42 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +module BaseTest + class API < ActionWebService::API::Base + api_method :add, :expects => [:int, :int], :returns => [:int] + api_method :void + end + + class PristineAPI < ActionWebService::API::Base + inflect_names false + + api_method :add + api_method :under_score + end + + class Service < ActionWebService::Base + web_service_api API + + def add(a, b) + end + + def void + end + end + + class PristineService < ActionWebService::Base + web_service_api PristineAPI + + def add + end + + def under_score + end + end +end + +class TC_Base < Test::Unit::TestCase + def test_options + assert(BaseTest::PristineService.web_service_api.inflect_names == false) + assert(BaseTest::Service.web_service_api.inflect_names == true) + end +end diff --git a/vendor/rails/actionwebservice/test/casting_test.rb b/vendor/rails/actionwebservice/test/casting_test.rb new file mode 100644 index 00000000..34bad07d --- /dev/null +++ b/vendor/rails/actionwebservice/test/casting_test.rb @@ -0,0 +1,86 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +module CastingTest + class API < ActionWebService::API::Base + api_method :int, :expects => [:int] + api_method :str, :expects => [:string] + api_method :base64, :expects => [:base64] + api_method :bool, :expects => [:bool] + api_method :float, :expects => [:float] + api_method :time, :expects => [:time] + api_method :datetime, :expects => [:datetime] + api_method :date, :expects => [:date] + + api_method :int_array, :expects => [[:int]] + api_method :str_array, :expects => [[:string]] + api_method :bool_array, :expects => [[:bool]] + end +end + +class TC_Casting < Test::Unit::TestCase + include CastingTest + + def test_base_type_casting_valid + assert_equal 10000, cast_expects(:int, '10000')[0] + assert_equal '10000', cast_expects(:str, 10000)[0] + base64 = cast_expects(:base64, 10000)[0] + assert_equal '10000', base64 + assert_instance_of ActionWebService::Base64, base64 + [1, '1', 'true', 'y', 'yes'].each do |val| + assert_equal true, cast_expects(:bool, val)[0] + end + [0, '0', 'false', 'n', 'no'].each do |val| + assert_equal false, cast_expects(:bool, val)[0] + end + assert_equal 3.14159, cast_expects(:float, '3.14159')[0] + now = Time.at(Time.now.tv_sec) + casted = cast_expects(:time, now.to_s)[0] + assert_equal now, casted + now = DateTime.now + assert_equal now.to_s, cast_expects(:datetime, now.to_s)[0].to_s + today = Date.today + assert_equal today, cast_expects(:date, today.to_s)[0] + end + + def test_base_type_casting_invalid + assert_raises ArgumentError do + cast_expects(:int, 'this is not a number') + end + assert_raises ActionWebService::Casting::CastingError do + # neither true or false ;) + cast_expects(:bool, 'i always lie') + end + assert_raises ArgumentError do + cast_expects(:float, 'not a float') + end + assert_raises ArgumentError do + cast_expects(:time, '111111111111111111111111111111111') + end + assert_raises ArgumentError do + cast_expects(:datetime, '-1') + end + assert_raises ArgumentError do + cast_expects(:date, '') + end + end + + def test_array_type_casting + assert_equal [1, 2, 3213992, 4], cast_expects(:int_array, ['1', '2', '3213992', '4'])[0] + assert_equal ['one', 'two', '5.0', '200', nil, 'true'], cast_expects(:str_array, [:one, 'two', 5.0, 200, nil, true])[0] + assert_equal [true, nil, true, true, false], cast_expects(:bool_array, ['1', nil, 'y', true, 'false'])[0] + end + + def test_array_type_casting_failure + assert_raises ActionWebService::Casting::CastingError do + cast_expects(:bool_array, ['false', 'blahblah']) + end + assert_raises ArgumentError do + cast_expects(:int_array, ['1', '2.021', '4']) + end + end + + private + def cast_expects(method_name, *args) + API.api_method_instance(method_name.to_sym).cast_expects([*args]) + end +end diff --git a/vendor/rails/actionwebservice/test/client_soap_test.rb b/vendor/rails/actionwebservice/test/client_soap_test.rb new file mode 100644 index 00000000..c03c2414 --- /dev/null +++ b/vendor/rails/actionwebservice/test/client_soap_test.rb @@ -0,0 +1,152 @@ +require File.dirname(__FILE__) + '/abstract_client' + + +module ClientSoapTest + PORT = 8998 + + class SoapClientLet < ClientTest::AbstractClientLet + def do_POST(req, res) + test_request = ActionController::TestRequest.new + test_request.request_parameters['action'] = req.path.gsub(/^\//, '').split(/\//)[1] + test_request.env['REQUEST_METHOD'] = "POST" + test_request.env['HTTP_CONTENTTYPE'] = 'text/xml' + test_request.env['HTTP_SOAPACTION'] = req.header['soapaction'][0] + test_request.env['RAW_POST_DATA'] = req.body + response = ActionController::TestResponse.new + @controller.process(test_request, response) + res.header['content-type'] = 'text/xml' + res.body = response.body + rescue Exception => e + $stderr.puts e.message + $stderr.puts e.backtrace.join("\n") + end + end + + class ClientContainer < ActionController::Base + web_client_api :client, :soap, "http://localhost:#{PORT}/client/api", :api => ClientTest::API + web_client_api :invalid, :null, "", :api => true + + def get_client + client + end + + def get_invalid + invalid + end + end + + class SoapServer < ClientTest::AbstractServer + def create_clientlet(controller) + SoapClientLet.new(controller) + end + + def server_port + PORT + end + end +end + +class TC_ClientSoap < Test::Unit::TestCase + include ClientTest + include ClientSoapTest + + fixtures :users + + def setup + @server = SoapServer.instance + @container = @server.container + @client = ActionWebService::Client::Soap.new(API, "http://localhost:#{@server.server_port}/client/api") + end + + def test_void + assert(@container.value_void.nil?) + @client.void + assert(!@container.value_void.nil?) + end + + def test_normal + assert(@container.value_normal.nil?) + assert_equal(5, @client.normal(5, 6)) + assert_equal([5, 6], @container.value_normal) + assert_equal(5, @client.normal("7", "8")) + assert_equal([7, 8], @container.value_normal) + assert_equal(5, @client.normal(true, false)) + end + + def test_array_return + assert(@container.value_array_return.nil?) + new_person = Person.new + new_person.firstnames = ["one", "two"] + new_person.lastname = "last" + assert_equal([new_person], @client.array_return) + assert_equal([new_person], @container.value_array_return) + end + + def test_struct_pass + assert(@container.value_struct_pass.nil?) + new_person = Person.new + new_person.firstnames = ["one", "two"] + new_person.lastname = "last" + assert_equal(true, @client.struct_pass([new_person])) + assert_equal([[new_person]], @container.value_struct_pass) + end + + def test_nil_struct_return + assert_nil @client.nil_struct_return + end + + def test_inner_nil + outer = @client.inner_nil + assert_equal 'outer', outer.name + assert_nil outer.inner + end + + def test_client_container + assert_equal(50, ClientContainer.new.get_client.client_container) + assert(ClientContainer.new.get_invalid.nil?) + end + + def test_named_parameters + assert(@container.value_named_parameters.nil?) + assert(@client.named_parameters("key", 5).nil?) + assert_equal(["key", 5], @container.value_named_parameters) + end + + def test_capitalized_method_name + @container.value_normal = nil + assert_equal(5, @client.Normal(5, 6)) + assert_equal([5, 6], @container.value_normal) + @container.value_normal = nil + end + + def test_model_return + user = @client.user_return + assert_equal 1, user.id + assert_equal 'Kent', user.name + assert user.active? + assert_kind_of Date, user.created_on + assert_equal Date.today, user.created_on + end + + def test_with_model + with_model = @client.with_model_return + assert_equal 'Kent', with_model.user.name + assert_equal 2, with_model.users.size + with_model.users.each do |user| + assert_kind_of User, user + end + end + + def test_scoped_model_return + scoped_model = @client.scoped_model_return + assert_kind_of Accounting::User, scoped_model + assert_equal 'Kent', scoped_model.name + end + + def test_multi_dim_return + md_struct = @client.multi_dim_return + assert_kind_of Array, md_struct.pref + assert_equal 2, md_struct.pref.size + assert_kind_of Array, md_struct.pref[0] + end +end diff --git a/vendor/rails/actionwebservice/test/client_xmlrpc_test.rb b/vendor/rails/actionwebservice/test/client_xmlrpc_test.rb new file mode 100644 index 00000000..0abd5898 --- /dev/null +++ b/vendor/rails/actionwebservice/test/client_xmlrpc_test.rb @@ -0,0 +1,151 @@ +require File.dirname(__FILE__) + '/abstract_client' + + +module ClientXmlRpcTest + PORT = 8999 + + class XmlRpcClientLet < ClientTest::AbstractClientLet + def do_POST(req, res) + test_request = ActionController::TestRequest.new + test_request.request_parameters['action'] = req.path.gsub(/^\//, '').split(/\//)[1] + test_request.env['REQUEST_METHOD'] = "POST" + test_request.env['HTTP_CONTENT_TYPE'] = 'text/xml' + test_request.env['RAW_POST_DATA'] = req.body + response = ActionController::TestResponse.new + @controller.process(test_request, response) + res.header['content-type'] = 'text/xml' + res.body = response.body + # puts res.body + rescue Exception => e + $stderr.puts e.message + $stderr.puts e.backtrace.join("\n") + end + end + + class ClientContainer < ActionController::Base + web_client_api :client, :xmlrpc, "http://localhost:#{PORT}/client/api", :api => ClientTest::API + + def get_client + client + end + end + + class XmlRpcServer < ClientTest::AbstractServer + def create_clientlet(controller) + XmlRpcClientLet.new(controller) + end + + def server_port + PORT + end + end +end + +class TC_ClientXmlRpc < Test::Unit::TestCase + include ClientTest + include ClientXmlRpcTest + + fixtures :users + + def setup + @server = XmlRpcServer.instance + @container = @server.container + @client = ActionWebService::Client::XmlRpc.new(API, "http://localhost:#{@server.server_port}/client/api") + end + + def test_void + assert(@container.value_void.nil?) + @client.void + assert(!@container.value_void.nil?) + end + + def test_normal + assert(@container.value_normal.nil?) + assert_equal(5, @client.normal(5, 6)) + assert_equal([5, 6], @container.value_normal) + assert_equal(5, @client.normal("7", "8")) + assert_equal([7, 8], @container.value_normal) + assert_equal(5, @client.normal(true, false)) + end + + def test_array_return + assert(@container.value_array_return.nil?) + new_person = Person.new + new_person.firstnames = ["one", "two"] + new_person.lastname = "last" + assert_equal([new_person], @client.array_return) + assert_equal([new_person], @container.value_array_return) + end + + def test_struct_pass + assert(@container.value_struct_pass.nil?) + new_person = Person.new + new_person.firstnames = ["one", "two"] + new_person.lastname = "last" + assert_equal(true, @client.struct_pass([new_person])) + assert_equal([[new_person]], @container.value_struct_pass) + end + + def test_nil_struct_return + assert_equal false, @client.nil_struct_return + end + + def test_inner_nil + outer = @client.inner_nil + assert_equal 'outer', outer.name + assert_nil outer.inner + end + + def test_client_container + assert_equal(50, ClientContainer.new.get_client.client_container) + end + + def test_named_parameters + assert(@container.value_named_parameters.nil?) + assert_equal(false, @client.named_parameters("xxx", 7)) + assert_equal(["xxx", 7], @container.value_named_parameters) + end + + def test_exception + assert_raises(ActionWebService::Client::ClientError) do + assert(@client.thrower) + end + end + + def test_invalid_signature + assert_raises(ArgumentError) do + @client.normal + end + end + + def test_model_return + user = @client.user_return + assert_equal 1, user.id + assert_equal 'Kent', user.name + assert user.active? + assert_kind_of Time, user.created_on + assert_equal Time.utc(Time.now.year, Time.now.month, Time.now.day), user.created_on + end + + def test_with_model + with_model = @client.with_model_return + assert_equal 'Kent', with_model.user.name + assert_equal 2, with_model.users.size + with_model.users.each do |user| + assert_kind_of User, user + end + end + + def test_scoped_model_return + scoped_model = @client.scoped_model_return + assert_kind_of Accounting::User, scoped_model + assert_equal 'Kent', scoped_model.name + end + + def test_multi_dim_return + md_struct = @client.multi_dim_return + assert_kind_of Array, md_struct.pref + assert_equal 2, md_struct.pref.size + assert_kind_of Array, md_struct.pref[0] + end +end diff --git a/vendor/rails/actionwebservice/test/container_test.rb b/vendor/rails/actionwebservice/test/container_test.rb new file mode 100644 index 00000000..325d420f --- /dev/null +++ b/vendor/rails/actionwebservice/test/container_test.rb @@ -0,0 +1,73 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +module ContainerTest + $immediate_service = Object.new + $deferred_service = Object.new + + class DelegateContainer < ActionController::Base + web_service_dispatching_mode :delegated + + attr :flag + attr :previous_flag + + def initialize + @previous_flag = nil + @flag = true + end + + web_service :immediate_service, $immediate_service + web_service(:deferred_service) { @previous_flag = @flag; @flag = false; $deferred_service } + end + + class DirectContainer < ActionController::Base + web_service_dispatching_mode :direct + end + + class InvalidContainer + include ActionWebService::Container::Direct + end +end + +class TC_Container < Test::Unit::TestCase + include ContainerTest + + def setup + @delegate_container = DelegateContainer.new + @direct_container = DirectContainer.new + end + + def test_registration + assert(DelegateContainer.has_web_service?(:immediate_service)) + assert(DelegateContainer.has_web_service?(:deferred_service)) + assert(!DelegateContainer.has_web_service?(:fake_service)) + assert_raises(ActionWebService::Container::Delegated::ContainerError) do + DelegateContainer.web_service('invalid') + end + end + + def test_service_object + assert_raises(ActionWebService::Container::Delegated::ContainerError) do + @delegate_container.web_service_object(:nonexistent) + end + assert(@delegate_container.flag == true) + assert(@delegate_container.web_service_object(:immediate_service) == $immediate_service) + assert(@delegate_container.previous_flag.nil?) + assert(@delegate_container.flag == true) + assert(@delegate_container.web_service_object(:deferred_service) == $deferred_service) + assert(@delegate_container.previous_flag == true) + assert(@delegate_container.flag == false) + end + + def test_direct_container + assert(DirectContainer.web_service_dispatching_mode == :direct) + end + + def test_validity + assert_raises(ActionWebService::Container::Direct::ContainerError) do + InvalidContainer.web_service_api :test + end + assert_raises(ActionWebService::Container::Direct::ContainerError) do + InvalidContainer.web_service_api 50.0 + end + end +end diff --git a/vendor/rails/actionwebservice/test/dispatcher_action_controller_soap_test.rb b/vendor/rails/actionwebservice/test/dispatcher_action_controller_soap_test.rb new file mode 100644 index 00000000..681c7c54 --- /dev/null +++ b/vendor/rails/actionwebservice/test/dispatcher_action_controller_soap_test.rb @@ -0,0 +1,139 @@ +$:.unshift(File.dirname(__FILE__) + '/apis') +require File.dirname(__FILE__) + '/abstract_dispatcher' +require 'wsdl/parser' + +class ActionController::Base + class << self + alias :inherited_without_name_error :inherited + def inherited(child) + begin + inherited_without_name_error(child) + rescue NameError => e + end + end + end +end + +class AutoLoadController < ActionController::Base; end +class FailingAutoLoadController < ActionController::Base; end +class BrokenAutoLoadController < ActionController::Base; end + +class TC_DispatcherActionControllerSoap < Test::Unit::TestCase + include DispatcherTest + include DispatcherCommonTests + + def setup + @direct_controller = DirectController.new + @delegated_controller = DelegatedController.new + @virtual_controller = VirtualController.new + @layered_controller = LayeredController.new + @protocol = ActionWebService::Protocol::Soap::SoapProtocol.create(@direct_controller) + end + + def test_wsdl_generation + ensure_valid_wsdl_generation DelegatedController.new, DispatcherTest::WsdlNamespace + ensure_valid_wsdl_generation DirectController.new, DispatcherTest::WsdlNamespace + end + + def test_wsdl_action + delegated_types = ensure_valid_wsdl_action DelegatedController.new + delegated_names = delegated_types.map{|x| x.name.name} + assert(delegated_names.include?('DispatcherTest..NodeArray')) + assert(delegated_names.include?('DispatcherTest..Node')) + direct_types = ensure_valid_wsdl_action DirectController.new + direct_names = direct_types.map{|x| x.name.name} + assert(direct_names.include?('DispatcherTest..NodeArray')) + assert(direct_names.include?('DispatcherTest..Node')) + assert(direct_names.include?('IntegerArray')) + end + + def test_autoloading + assert(!AutoLoadController.web_service_api.nil?) + assert(AutoLoadController.web_service_api.has_public_api_method?('Void')) + assert(FailingAutoLoadController.web_service_api.nil?) + assert_raises(MissingSourceFile) do + FailingAutoLoadController.require_web_service_api :blah + end + assert_raises(ArgumentError) do + FailingAutoLoadController.require_web_service_api 50.0 + end + assert(BrokenAutoLoadController.web_service_api.nil?) + end + + def test_layered_dispatching + mt_cats = do_method_call(@layered_controller, 'mt.getCategories') + assert_equal(["mtCat1", "mtCat2"], mt_cats) + blogger_cats = do_method_call(@layered_controller, 'blogger.getCategories') + assert_equal(["bloggerCat1", "bloggerCat2"], blogger_cats) + end + + def test_utf8 + @direct_controller.web_service_exception_reporting = true + $KCODE = 'u' + assert_equal(Utf8String, do_method_call(@direct_controller, 'TestUtf8')) + retval = SOAP::Processor.unmarshal(@response_body).body.response + assert retval.is_a?(SOAP::SOAPString) + + # If $KCODE is not set to UTF-8, any strings with non-ASCII UTF-8 data + # will be sent back as base64 by SOAP4R. By the time we get it here though, + # it will be decoded back into a string. So lets read the base64 value + # from the message body directly. + $KCODE = 'NONE' + do_method_call(@direct_controller, 'TestUtf8') + retval = SOAP::Processor.unmarshal(@response_body).body.response + assert retval.is_a?(SOAP::SOAPBase64) + assert_equal "T25lIFdvcmxkIENhZsOp", retval.data.to_s + end + + protected + def exception_message(soap_fault_exception) + soap_fault_exception.detail.cause.message + end + + def is_exception?(obj) + obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && \ + obj.detail.cause.is_a?(Exception) + end + + def service_name(container) + container.is_a?(DelegatedController) ? 'test_service' : 'api' + end + + def ensure_valid_wsdl_generation(controller, expected_namespace) + wsdl = controller.generate_wsdl + ensure_valid_wsdl(controller, wsdl, expected_namespace) + end + + def ensure_valid_wsdl(controller, wsdl, expected_namespace) + definitions = WSDL::Parser.new.parse(wsdl) + assert(definitions.is_a?(WSDL::Definitions)) + definitions.bindings.each do |binding| + assert(binding.name.name.index(':').nil?) + end + definitions.services.each do |service| + service.ports.each do |port| + assert(port.name.name.index(':').nil?) + end + end + types = definitions.collect_complextypes.map{|x| x.name} + types.each do |type| + assert(type.namespace == expected_namespace) + end + location = definitions.services[0].ports[0].soap_address.location + if controller.is_a?(DelegatedController) + assert_match %r{http://localhost/dispatcher_test/delegated/test_service$}, location + elsif controller.is_a?(DirectController) + assert_match %r{http://localhost/dispatcher_test/direct/api$}, location + end + definitions.collect_complextypes + end + + def ensure_valid_wsdl_action(controller) + test_request = ActionController::TestRequest.new({ 'action' => 'wsdl' }) + test_request.env['REQUEST_METHOD'] = 'GET' + test_request.env['HTTP_HOST'] = 'localhost' + test_response = ActionController::TestResponse.new + wsdl = controller.process(test_request, test_response).body + ensure_valid_wsdl(controller, wsdl, DispatcherTest::WsdlNamespace) + end +end diff --git a/vendor/rails/actionwebservice/test/dispatcher_action_controller_xmlrpc_test.rb b/vendor/rails/actionwebservice/test/dispatcher_action_controller_xmlrpc_test.rb new file mode 100644 index 00000000..95c93339 --- /dev/null +++ b/vendor/rails/actionwebservice/test/dispatcher_action_controller_xmlrpc_test.rb @@ -0,0 +1,57 @@ +require File.dirname(__FILE__) + '/abstract_dispatcher' + +class TC_DispatcherActionControllerXmlRpc < Test::Unit::TestCase + include DispatcherTest + include DispatcherCommonTests + + def setup + @direct_controller = DirectController.new + @delegated_controller = DelegatedController.new + @layered_controller = LayeredController.new + @virtual_controller = VirtualController.new + @protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.create(@direct_controller) + end + + def test_layered_dispatching + mt_cats = do_method_call(@layered_controller, 'mt.getCategories') + assert_equal(["mtCat1", "mtCat2"], mt_cats) + blogger_cats = do_method_call(@layered_controller, 'blogger.getCategories') + assert_equal(["bloggerCat1", "bloggerCat2"], blogger_cats) + end + + def test_multicall + response = do_method_call(@layered_controller, 'system.multicall', [ + {'methodName' => 'mt.getCategories'}, + {'methodName' => 'blogger.getCategories'}, + {'methodName' => 'mt.bool'}, + {'methodName' => 'blogger.str', 'params' => ['2000']}, + {'methodName' => 'mt.alwaysFail'}, + {'methodName' => 'blogger.alwaysFail'}, + {'methodName' => 'mt.blah'}, + {'methodName' => 'blah.blah'} + ]) + assert_equal [ + [["mtCat1", "mtCat2"]], + [["bloggerCat1", "bloggerCat2"]], + [true], + ["2500"], + {"faultCode" => 3, "faultString" => "MT AlwaysFail"}, + {"faultCode" => 3, "faultString" => "Blogger AlwaysFail"}, + {"faultCode" => 4, "faultMessage" => "no such method 'blah' on API DispatcherTest::MTAPI"}, + {"faultCode" => 4, "faultMessage" => "no such web service 'blah'"} + ], response + end + + protected + def exception_message(xmlrpc_fault_exception) + xmlrpc_fault_exception.faultString + end + + def is_exception?(obj) + obj.is_a?(XMLRPC::FaultException) + end + + def service_name(container) + container.is_a?(DelegatedController) ? 'test_service' : 'api' + end +end diff --git a/vendor/rails/actionwebservice/test/fixtures/db_definitions/mysql.sql b/vendor/rails/actionwebservice/test/fixtures/db_definitions/mysql.sql new file mode 100644 index 00000000..026f2a2c --- /dev/null +++ b/vendor/rails/actionwebservice/test/fixtures/db_definitions/mysql.sql @@ -0,0 +1,7 @@ +CREATE TABLE `users` ( + `id` int(11) NOT NULL auto_increment, + `name` varchar(30) default NULL, + `active` tinyint(4) default NULL, + `created_on` date default NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=latin1; diff --git a/vendor/rails/actionwebservice/test/fixtures/users.yml b/vendor/rails/actionwebservice/test/fixtures/users.yml new file mode 100644 index 00000000..a97d8c84 --- /dev/null +++ b/vendor/rails/actionwebservice/test/fixtures/users.yml @@ -0,0 +1,10 @@ +user1: + id: 1 + name: Kent + active: 1 + created_on: <%= Date.today %> +user2: + id: 2 + name: David + active: 1 + created_on: <%= Date.today %> diff --git a/vendor/rails/actionwebservice/test/gencov b/vendor/rails/actionwebservice/test/gencov new file mode 100755 index 00000000..1faab34c --- /dev/null +++ b/vendor/rails/actionwebservice/test/gencov @@ -0,0 +1,3 @@ +#!/bin/sh + +rcov -x '.*_test\.rb,rubygems,abstract_,/run,/apis' ./run diff --git a/vendor/rails/actionwebservice/test/invocation_test.rb b/vendor/rails/actionwebservice/test/invocation_test.rb new file mode 100644 index 00000000..3ef22faf --- /dev/null +++ b/vendor/rails/actionwebservice/test/invocation_test.rb @@ -0,0 +1,185 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +module InvocationTest + class API < ActionWebService::API::Base + api_method :add, :expects => [:int, :int], :returns => [:int] + api_method :transmogrify, :expects_and_returns => [:string] + api_method :fail_with_reason + api_method :fail_generic + api_method :no_before + api_method :no_after + api_method :only_one + api_method :only_two + end + + class Interceptor + attr :args + + def initialize + @args = nil + end + + def intercept(*args) + @args = args + end + end + + InterceptorClass = Interceptor.new + + class Service < ActionController::Base + web_service_api API + + before_invocation :intercept_before, :except => [:no_before] + after_invocation :intercept_after, :except => [:no_after] + prepend_after_invocation :intercept_after_first, :except => [:no_after] + prepend_before_invocation :intercept_only, :only => [:only_one, :only_two] + after_invocation(:only => [:only_one]) do |*args| + args[0].instance_variable_set('@block_invoked', args[1]) + end + after_invocation InterceptorClass, :only => [:only_one] + + attr_accessor :before_invoked + attr_accessor :after_invoked + attr_accessor :after_first_invoked + attr_accessor :only_invoked + attr_accessor :block_invoked + attr_accessor :invocation_result + + def initialize + @before_invoked = nil + @after_invoked = nil + @after_first_invoked = nil + @only_invoked = nil + @invocation_result = nil + @block_invoked = nil + end + + def add(a, b) + a + b + end + + def transmogrify(str) + str.upcase + end + + def fail_with_reason + end + + def fail_generic + end + + def no_before + 5 + end + + def no_after + end + + def only_one + end + + def only_two + end + + protected + def intercept_before(name, args) + @before_invoked = name + return [false, "permission denied"] if name == :fail_with_reason + return false if name == :fail_generic + end + + def intercept_after(name, args, result) + @after_invoked = name + @invocation_result = result + end + + def intercept_after_first(name, args, result) + @after_first_invoked = name + end + + def intercept_only(name, args) + raise "Interception error" unless name == :only_one || name == :only_two + @only_invoked = name + end + end +end + +class TC_Invocation < Test::Unit::TestCase + include ActionWebService::Invocation + + def setup + @service = InvocationTest::Service.new + end + + def test_invocation + assert(perform_invocation(:add, 5, 10) == 15) + assert(perform_invocation(:transmogrify, "hello") == "HELLO") + assert_raises(NoMethodError) do + perform_invocation(:nonexistent_method_xyzzy) + end + end + + def test_interceptor_registration + assert(InvocationTest::Service.before_invocation_interceptors.length == 2) + assert(InvocationTest::Service.after_invocation_interceptors.length == 4) + assert_equal(:intercept_only, InvocationTest::Service.before_invocation_interceptors[0]) + assert_equal(:intercept_after_first, InvocationTest::Service.after_invocation_interceptors[0]) + end + + def test_interception + assert(@service.before_invoked.nil?) + assert(@service.after_invoked.nil?) + assert(@service.only_invoked.nil?) + assert(@service.block_invoked.nil?) + assert(@service.invocation_result.nil?) + perform_invocation(:add, 20, 50) + assert(@service.before_invoked == :add) + assert(@service.after_invoked == :add) + assert(@service.invocation_result == 70) + end + + def test_interception_canceling + reason = nil + perform_invocation(:fail_with_reason){|r| reason = r} + assert(@service.before_invoked == :fail_with_reason) + assert(@service.after_invoked.nil?) + assert(@service.invocation_result.nil?) + assert(reason == "permission denied") + reason = true + @service.before_invoked = @service.after_invoked = @service.invocation_result = nil + perform_invocation(:fail_generic){|r| reason = r} + assert(@service.before_invoked == :fail_generic) + assert(@service.after_invoked.nil?) + assert(@service.invocation_result.nil?) + assert(reason == true) + end + + def test_interception_except_conditions + perform_invocation(:no_before) + assert(@service.before_invoked.nil?) + assert(@service.after_first_invoked == :no_before) + assert(@service.after_invoked == :no_before) + assert(@service.invocation_result == 5) + @service.before_invoked = @service.after_invoked = @service.invocation_result = nil + perform_invocation(:no_after) + assert(@service.before_invoked == :no_after) + assert(@service.after_invoked.nil?) + assert(@service.invocation_result.nil?) + end + + def test_interception_only_conditions + assert(@service.only_invoked.nil?) + perform_invocation(:only_one) + assert(@service.only_invoked == :only_one) + assert(@service.block_invoked == :only_one) + assert(InvocationTest::InterceptorClass.args[1] == :only_one) + @service.only_invoked = nil + perform_invocation(:only_two) + assert(@service.only_invoked == :only_two) + end + + private + def perform_invocation(method_name, *args, &block) + @service.perform_invocation(method_name, args, &block) + end +end diff --git a/vendor/rails/actionwebservice/test/run b/vendor/rails/actionwebservice/test/run new file mode 100755 index 00000000..c8c03727 --- /dev/null +++ b/vendor/rails/actionwebservice/test/run @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require 'test/unit' +$:.unshift(File.dirname(__FILE__) + '/../lib') +args = Dir[File.join(File.dirname(__FILE__), '*_test.rb')] + Dir[File.join(File.dirname(__FILE__), 'ws/*_test.rb')] +(r = Test::Unit::AutoRunner.new(true)).process_args(args) +exit r.run diff --git a/vendor/rails/actionwebservice/test/scaffolded_controller_test.rb b/vendor/rails/actionwebservice/test/scaffolded_controller_test.rb new file mode 100644 index 00000000..123a7043 --- /dev/null +++ b/vendor/rails/actionwebservice/test/scaffolded_controller_test.rb @@ -0,0 +1,145 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +ActionController::Routing::Routes.draw do |map| + map.connect '', :controller => 'scaffolded' + map.connect ':controller/:action/:id' +end + +ActionController::Base.template_root = '.' + +class ScaffoldPerson < ActionWebService::Struct + member :id, :int + member :name, :string + member :birth, :date + + def ==(other) + self.id == other.id && self.name == other.name + end +end + +class ScaffoldedControllerTestAPI < ActionWebService::API::Base + api_method :hello, :expects => [{:integer=>:int}, :string], :returns => [:bool] + api_method :hello_struct_param, :expects => [{:person => ScaffoldPerson}], :returns => [:bool] + api_method :date_of_birth, :expects => [ScaffoldPerson], :returns => [:string] + api_method :bye, :returns => [[ScaffoldPerson]] + api_method :date_diff, :expects => [{:start_date => :date}, {:end_date => :date}], :returns => [:int] + api_method :time_diff, :expects => [{:start_time => :time}, {:end_time => :time}], :returns => [:int] + api_method :base64_upcase, :expects => [:base64], :returns => [:base64] +end + +class ScaffoldedController < ActionController::Base + web_service_api ScaffoldedControllerTestAPI + web_service_scaffold :scaffold_invoke + + def hello(int, string) + 0 + end + + def hello_struct_param(person) + 0 + end + + def date_of_birth(person) + person.birth.to_s + end + + def bye + [ScaffoldPerson.new(:id => 1, :name => "leon"), ScaffoldPerson.new(:id => 2, :name => "paul")] + end + + def rescue_action(e) + raise e + end + + def date_diff(start_date, end_date) + end_date - start_date + end + + def time_diff(start_time, end_time) + end_time - start_time + end + + def base64_upcase(data) + data.upcase + end +end + +class ScaffoldedControllerTest < Test::Unit::TestCase + def setup + @controller = ScaffoldedController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_scaffold_invoke + get :scaffold_invoke + assert_rendered_file 'methods.rhtml' + end + + def test_scaffold_invoke_method_params + get :scaffold_invoke_method_params, :service => 'scaffolded', :method => 'Hello' + assert_rendered_file 'parameters.rhtml' + end + + def test_scaffold_invoke_method_params_with_struct + get :scaffold_invoke_method_params, :service => 'scaffolded', :method => 'HelloStructParam' + assert_rendered_file 'parameters.rhtml' + assert_tag :tag => 'input', :attributes => {:name => "method_params[0][name]"} + end + + def test_scaffold_invoke_submit_hello + post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'Hello', :method_params => {'0' => '5', '1' => 'hello world'} + assert_rendered_file 'result.rhtml' + assert_equal false, @controller.instance_eval{ @method_return_value } + end + + def test_scaffold_invoke_submit_bye + post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'Bye' + assert_rendered_file 'result.rhtml' + persons = [ScaffoldPerson.new(:id => 1, :name => "leon"), ScaffoldPerson.new(:id => 2, :name => "paul")] + assert_equal persons, @controller.instance_eval{ @method_return_value } + end + + def test_scaffold_date_params + get :scaffold_invoke_method_params, :service => 'scaffolded', :method => 'DateDiff' + (0..1).each do |param| + (1..3).each do |date_part| + assert_tag :tag => 'select', :attributes => {:name => "method_params[#{param}][#{date_part}]"}, + :children => {:greater_than => 1, :only => {:tag => 'option'}} + end + end + + post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'DateDiff', + :method_params => {'0' => {'1' => '2006', '2' => '2', '3' => '1'}, '1' => {'1' => '2006', '2' => '2', '3' => '2'}} + assert_equal 1, @controller.instance_eval{ @method_return_value } + end + + def test_scaffold_struct_date_params + post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'DateOfBirth', + :method_params => {'0' => {'birth' => {'1' => '2006', '2' => '2', '3' => '1'}, 'id' => '1', 'name' => 'person'}} + assert_equal '2006-02-01', @controller.instance_eval{ @method_return_value } + end + + def test_scaffold_time_params + get :scaffold_invoke_method_params, :service => 'scaffolded', :method => 'TimeDiff' + (0..1).each do |param| + (1..6).each do |date_part| + assert_tag :tag => 'select', :attributes => {:name => "method_params[#{param}][#{date_part}]"}, + :children => {:greater_than => 1, :only => {:tag => 'option'}} + end + end + + post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'TimeDiff', + :method_params => {'0' => {'1' => '2006', '2' => '2', '3' => '1', '4' => '1', '5' => '1', '6' => '1'}, + '1' => {'1' => '2006', '2' => '2', '3' => '2', '4' => '1', '5' => '1', '6' => '1'}} + assert_equal 86400, @controller.instance_eval{ @method_return_value } + end + + def test_scaffold_base64 + get :scaffold_invoke_method_params, :service => 'scaffolded', :method => 'Base64Upcase' + assert_tag :tag => 'textarea', :attributes => {:name => 'method_params[0]'} + + post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'Base64Upcase', :method_params => {'0' => 'scaffold'} + assert_equal 'SCAFFOLD', @controller.instance_eval{ @method_return_value } + end +end diff --git a/vendor/rails/actionwebservice/test/struct_test.rb b/vendor/rails/actionwebservice/test/struct_test.rb new file mode 100644 index 00000000..f689746e --- /dev/null +++ b/vendor/rails/actionwebservice/test/struct_test.rb @@ -0,0 +1,52 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +module StructTest + class Struct < ActionWebService::Struct + member :id, Integer + member :name, String + member :items, [String] + member :deleted, :bool + member :emails, [:string] + end +end + +class TC_Struct < Test::Unit::TestCase + include StructTest + + def setup + @struct = Struct.new(:id => 5, + :name => 'hello', + :items => ['one', 'two'], + :deleted => true, + :emails => ['test@test.com']) + end + + def test_members + assert_equal(5, Struct.members.size) + assert_equal(Integer, Struct.members[:id].type_class) + assert_equal(String, Struct.members[:name].type_class) + assert_equal(String, Struct.members[:items].element_type.type_class) + assert_equal(TrueClass, Struct.members[:deleted].type_class) + assert_equal(String, Struct.members[:emails].element_type.type_class) + end + + def test_initializer_and_lookup + assert_equal(5, @struct.id) + assert_equal('hello', @struct.name) + assert_equal(['one', 'two'], @struct.items) + assert_equal(true, @struct.deleted) + assert_equal(['test@test.com'], @struct.emails) + assert_equal(5, @struct['id']) + assert_equal('hello', @struct['name']) + assert_equal(['one', 'two'], @struct['items']) + assert_equal(true, @struct['deleted']) + assert_equal(['test@test.com'], @struct['emails']) + end + + def test_each_pair + @struct.each_pair do |name, value| + assert_equal @struct.__send__(name), value + assert_equal @struct[name], value + end + end +end diff --git a/vendor/rails/actionwebservice/test/test_invoke_test.rb b/vendor/rails/actionwebservice/test/test_invoke_test.rb new file mode 100644 index 00000000..46f9ddb2 --- /dev/null +++ b/vendor/rails/actionwebservice/test/test_invoke_test.rb @@ -0,0 +1,100 @@ +require File.dirname(__FILE__) + '/abstract_unit' +require 'action_web_service/test_invoke' + +class TestInvokeAPI < ActionWebService::API::Base + api_method :add, :expects => [:int, :int], :returns => [:int] +end + +class TestInvokeService < ActionWebService::Base + web_service_api TestInvokeAPI + + attr :invoked + + def add(a, b) + @invoked = true + a + b + end +end + +class TestController < ActionController::Base + def rescue_action(e); raise e; end +end + +class TestInvokeDirectController < TestController + web_service_api TestInvokeAPI + + attr :invoked + + def add + @invoked = true + @method_params[0] + @method_params[1] + end +end + +class TestInvokeDelegatedController < TestController + web_service_dispatching_mode :delegated + web_service :service, TestInvokeService.new +end + +class TestInvokeLayeredController < TestController + web_service_dispatching_mode :layered + web_service(:one) { @service_one ||= TestInvokeService.new } + web_service(:two) { @service_two ||= TestInvokeService.new } +end + +class TestInvokeTest < Test::Unit::TestCase + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_direct_add + @controller = TestInvokeDirectController.new + assert_equal nil, @controller.invoked + result = invoke :add, 25, 25 + assert_equal 50, result + assert_equal true, @controller.invoked + end + + def test_delegated_add + @controller = TestInvokeDelegatedController.new + assert_equal nil, @controller.web_service_object(:service).invoked + result = invoke_delegated :service, :add, 100, 50 + assert_equal 150, result + assert_equal true, @controller.web_service_object(:service).invoked + end + + def test_layered_add + [:soap, :xmlrpc].each do |protocol| + @protocol = protocol + [:one, :two].each do |service| + @controller = TestInvokeLayeredController.new + assert_equal nil, @controller.web_service_object(service).invoked + result = invoke_layered service, :add, 200, -50 + assert_equal 150, result + assert_equal true, @controller.web_service_object(service).invoked + end + end + end + + def test_layered_fail_with_wrong_number_of_arguments + [:soap, :xmlrpc].each do |protocol| + @protocol = protocol + [:one, :two].each do |service| + @controller = TestInvokeLayeredController.new + assert_raise(ArgumentError) { invoke_layered service, :add, 1 } + end + end + end + + def test_delegated_fail_with_wrong_number_of_arguments + @controller = TestInvokeDelegatedController.new + assert_raise(ArgumentError) { invoke_delegated :service, :add, 1 } + end + + def test_direct_fail_with_wrong_number_of_arguments + @controller = TestInvokeDirectController.new + assert_raise(ArgumentError) { invoke :add, 1 } + end + +end diff --git a/vendor/rails/activerecord/CHANGELOG b/vendor/rails/activerecord/CHANGELOG new file mode 100644 index 00000000..34db2bc1 --- /dev/null +++ b/vendor/rails/activerecord/CHANGELOG @@ -0,0 +1,2608 @@ +*1.14.4* (August 8th, 2006) + +* Add warning about the proper way to validate the presence of a foreign key. #4147 [Francois Beausoleil ] + +* Fix syntax error in documentation. #4679 [mislav@nippur.irb.hr] + +* Update inconsistent migrations documentation. #4683 [machomagna@gmail.com] + + +*1.14.3* (June 27th, 2006) + +* Fix announcement of very long migration names. #5722 [blake@near-time.com] + +* Update callbacks documentation. #3970 [Robby Russell ] + +* Properly quote index names in migrations (closes #4764) [John Long] + +* Ensure that Associations#include_eager_conditions? checks both scoped and explicit conditions [Rick] + +* Associations#select_limited_ids_list adds the ORDER BY columns to the SELECT DISTINCT List for postgresql. [Rick] + + +*1.14.2* (April 9th, 2006) + +* Fixed calculations for the Oracle Adapter (closes #4626) [Michael Schoen] + + +*1.14.1* (April 6th, 2006) + +* Fix type_name_with_module to handle type names that begin with '::'. Closes #4614. [Nicholas Seckar] + +* Fixed that that multiparameter assignment doesn't work with aggregations (closes #4620) [Lars Pind] + +* Enable Limit/Offset in Calculations (closes #4558) [lmarlow@yahoo.com] + +* Fixed that loading including associations returns all results if Load IDs For Limited Eager Loading returns none (closes #4528) [Rick] + +* Fixed HasManyAssociation#find bugs when :finder_sql is set #4600 [lagroue@free.fr] + +* Allow AR::Base#respond_to? to behave when @attributes is nil [zenspider] + +* Support eager includes when going through a polymorphic has_many association. [Rick] + +* Added support for eagerly including polymorphic has_one associations. (closes #4525) [Rick] + + class Post < ActiveRecord::Base + has_one :tagging, :as => :taggable + end + + Post.find :all, :include => :tagging + +* Added descriptive error messages for invalid has_many :through associations: going through :has_one or :has_and_belongs_to_many [Rick] + +* Added support for going through a polymorphic has_many association: (closes #4401) [Rick] + + class PhotoCollection < ActiveRecord::Base + has_many :photos, :as => :photographic + belongs_to :firm + end + + class Firm < ActiveRecord::Base + has_many :photo_collections + has_many :photos, :through => :photo_collections + end + +* Multiple fixes and optimizations in PostgreSQL adapter, allowing ruby-postgres gem to work properly. [ruben.nine@gmail.com] + +* Fixed that AssociationCollection#delete_all should work even if the records of the association are not loaded yet. [Florian Weber] + +* Changed those private ActiveRecord methods to take optional third argument :auto instead of nil for performance optimizations. (closes #4456) [Stefan] + +* Private ActiveRecord methods add_limit!, add_joins!, and add_conditions! take an OPTIONAL third argument 'scope' (closes #4456) [Rick] + +* DEPRECATED: Using additional attributes on has_and_belongs_to_many associations. Instead upgrade your association to be a real join model [DHH] + +* Fixed that records returned from has_and_belongs_to_many associations with additional attributes should be marked as read only (fixes #4512) [DHH] + +* Do not implicitly mark recordss of has_many :through as readonly but do mark habtm records as readonly (eventually only on join tables without rich attributes). [Marcel Mollina Jr.] + +* Fixed broken OCIAdapter #4457 [schoenm@earthlink.net] + + +*1.14.0* (March 27th, 2006) + +* Replace 'rescue Object' with a finer grained rescue. Closes #4431. [Nicholas Seckar] + +* Fixed eager loading so that an aliased table cannot clash with a has_and_belongs_to_many join table [Rick] + +* Add support for :include to with_scope [andrew@redlinesoftware.com] + +* Support the use of public synonyms with the Oracle adapter; required ruby-oci8 v0.1.14 #4390 [schoenm@earthlink.net] + +* Change periods (.) in table aliases to _'s. Closes #4251 [jeff@ministrycentered.com] + +* Changed has_and_belongs_to_many join to INNER JOIN for Mysql 3.23.x. Closes #4348 [Rick] + +* Fixed issue that kept :select options from being scoped [Rick] + +* Fixed db_schema_import when binary types are present #3101 [DHH] + +* Fixed that MySQL enums should always be returned as strings #3501 [DHH] + +* Change has_many :through to use the :source option to specify the source association. :class_name is now ignored. [Rick Olson] + + class Connection < ActiveRecord::Base + belongs_to :user + belongs_to :channel + end + + class Channel < ActiveRecord::Base + has_many :connections + has_many :contacts, :through => :connections, :class_name => 'User' # OLD + has_many :contacts, :through => :connections, :source => :user # NEW + end + +* Fixed DB2 adapter so nullable columns will be determines correctly now and quotes from column default values will be removed #4350 [contact@maik-schmidt.de] + +* Allow overriding of find parameters in scoped has_many :through calls [Rick Olson] + + In this example, :include => false disables the default eager association from loading. :select changes the standard + select clause. :joins specifies a join that is added to the end of the has_many :through query. + + class Post < ActiveRecord::Base + has_many :tags, :through => :taggings, :include => :tagging do + def add_joins_and_select + find :all, :select => 'tags.*, authors.id as author_id', :include => false, + :joins => 'left outer join posts on taggings.taggable_id = posts.id left outer join authors on posts.author_id = authors.id' + end + end + end + +* Fixed that schema changes while the database was open would break any connections to a SQLite database (now we reconnect if that error is throw) [DHH] + +* Don't classify the has_one class when eager loading, it is already singular. Add tests. (closes #4117) [jonathan@bluewire.net.nz] + +* Quit ignoring default :include options in has_many :through calls [Mark James] + +* Allow has_many :through associations to find the source association by setting a custom class (closes #4307) [jonathan@bluewire.net.nz] + +* Eager Loading support added for has_many :through => :has_many associations (see below). [Rick Olson] + +* Allow has_many :through to work on has_many associations (closes #3864) [sco@scottraymond.net] Example: + + class Firm < ActiveRecord::Base + has_many :clients + has_many :invoices, :through => :clients + end + + class Client < ActiveRecord::Base + belongs_to :firm + has_many :invoices + end + + class Invoice < ActiveRecord::Base + belongs_to :client + end + +* Raise error when trying to select many polymorphic objects with has_many :through or :include (closes #4226) [josh@hasmanythrough.com] + +* Fixed has_many :through to include :conditions set on the :through association. closes #4020 [jonathan@bluewire.net.nz] + +* Fix that has_many :through honors the foreign key set by the belongs_to association in the join model (closes #4259) [andylien@gmail.com / Rick] + +* SQL Server adapter gets some love #4298 [rtomayko@gmail.com] + +* Added OpenBase database adapter that builds on top of the http://www.spice-of-life.net/ruby-openbase/ driver. All functionality except LIMIT/OFFSET is supported #3528 [derrickspell@cdmplus.com] + +* Rework table aliasing to account for truncated table aliases. Add smarter table aliasing when doing eager loading of STI associations. This allows you to use the association name in the order/where clause. [Jonathan Viney / Rick Olson] #4108 Example (SpecialComment is using STI): + + Author.find(:all, :include => { :posts => :special_comments }, :order => 'special_comments.body') + +* Add AbstractAdapter#table_alias_for to create table aliases according to the rules of the current adapter. [Rick] + +* Provide access to the underlying database connection through Adapter#raw_connection. Enables the use of db-specific methods without complicating the adapters. #2090 [Koz] + +* Remove broken attempts at handling columns with a default of 'now()' in the postgresql adapter. #2257 [Koz] + +* Added connection#current_database that'll return of the current database (only works in MySQL, SQL Server, and Oracle so far -- please help implement for the rest of the adapters) #3663 [Tom ward] + +* Fixed that Migration#execute would have the table name prefix appended to its query #4110 [mark.imbriaco@pobox.com] + +* Make all tinyint(1) variants act like boolean in mysql (tinyint(1) unsigned, etc.) [Jamis Buck] + +* Use association's :conditions when eager loading. [jeremyevans0@gmail.com] #4144 + +* Alias the has_and_belongs_to_many join table on eager includes. #4106 [jeremyevans0@gmail.com] + + This statement would normally error because the projects_developers table is joined twice, and therefore joined_on would be ambiguous. + + Developer.find(:all, :include => {:projects => :developers}, :conditions => 'join_project_developers.joined_on IS NOT NULL') + +* Oracle adapter gets some love #4230 [schoenm@earthlink.net] + + * Changes :text to CLOB rather than BLOB [Moses Hohman] + * Fixes an issue with nil numeric length/scales (several) + * Implements support for XMLTYPE columns [wilig / Kubo Takehiro] + * Tweaks a unit test to get it all green again + * Adds support for #current_database + +* Added Base.abstract_class? that marks which classes are not part of the Active Record hierarchy #3704 [Rick Olson] + + class CachedModel < ActiveRecord::Base + self.abstract_class = true + end + + class Post < CachedModel + end + + CachedModel.abstract_class? + => true + + Post.abstract_class? + => false + + Post.base_class + => Post + + Post.table_name + => 'posts' + +* Allow :dependent options to be used with polymorphic joins. #3820 [Rick Olson] + + class Foo < ActiveRecord::Base + has_many :attachments, :as => :attachable, :dependent => :delete_all + end + +* Nicer error message on has_many :through when :through reflection can not be found. #4042 [court3nay@gmail.com] + +* Upgrade to Transaction::Simple 1.3 [Jamis Buck] + +* Catch FixtureClassNotFound when using instantiated fixtures on a fixture that has no ActiveRecord model [Rick Olson] + +* Allow ordering of calculated results and/or grouped fields in calculations [solo@gatelys.com] + +* Make ActiveRecord::Base#save! return true instead of nil on success. #4173 [johan@johansorensen.com] + +* Dynamically set allow_concurrency. #4044 [Stefan Kaes] + +* Added Base#to_xml that'll turn the current record into a XML representation [DHH]. Example: + + topic.to_xml + + ...returns: + + + + The First Topic + David + 1 + false + 0 + 2000-01-01 08:28:00 + 2003-07-16 09:28:00 + Have a nice day + david@loudthinking.com + + 2004-04-15 + + + ...and you can configure with: + + topic.to_xml(:skip_instruct => true, :except => [ :id, bonus_time, :written_on, replies_count ]) + + ...that'll return: + + + The First Topic + David + false + Have a nice day + david@loudthinking.com + + 2004-04-15 + + + You can even do load first-level associations as part of the document: + + firm.to_xml :include => [ :account, :clients ] + + ...that'll return something like: + + + + 1 + 1 + 37signals + + + 1 + Summit + + + 1 + Microsoft + + + + 1 + 50 + + + +* Allow :counter_cache to take a column name for custom counter cache columns [Jamis Buck] + +* Documentation fixes for :dependent [robby@planetargon.com] + +* Stop the MySQL adapter crashing when views are present. #3782 [Jonathan Viney] + +* Don't classify the belongs_to class, it is already singular #4117 [keithm@infused.org] + +* Allow set_fixture_class to take Classes instead of strings for a class in a module. Raise FixtureClassNotFound if a fixture can't load. [Rick Olson] + +* Fix quoting of inheritance column for STI eager loading #4098 [Jonathan Viney ] + +* Added smarter table aliasing for eager associations for multiple self joins #3580 [Rick Olson] + + * The first time a table is referenced in a join, no alias is used. + * After that, the parent class name and the reflection name are used. + + Tree.find(:all, :include => :children) # LEFT OUTER JOIN trees AS tree_children ... + + * Any additional join references get a numerical suffix like '_2', '_3', etc. + +* Fixed eager loading problems with single-table inheritance #3580 [Rick Olson]. Post.find(:all, :include => :special_comments) now returns all posts, and any special comments that the posts may have. And made STI work with has_many :through and polymorphic belongs_to. + +* Added cascading eager loading that allows for queries like Author.find(:all, :include=> { :posts=> :comments }), which will fetch all authors, their posts, and the comments belonging to those posts in a single query (using LEFT OUTER JOIN) #3913 [anna@wota.jp]. Examples: + + # cascaded in two levels + >> Author.find(:all, :include=>{:posts=>:comments}) + => authors + +- posts + +- comments + + # cascaded in two levels and normal association + >> Author.find(:all, :include=>[{:posts=>:comments}, :categorizations]) + => authors + +- posts + +- comments + +- categorizations + + # cascaded in two levels with two has_many associations + >> Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}) + => authors + +- posts + +- comments + +- categorizations + + # cascaded in three levels + >> Company.find(:all, :include=>{:groups=>{:members=>{:favorites}}}) + => companies + +- groups + +- members + +- favorites + +* Make counter cache work when replacing an association #3245 [eugenol@gmail.com] + +* Make migrations verbose [Jamis Buck] + +* Make counter_cache work with polymorphic belongs_to [Jamis Buck] + +* Fixed that calling HasOneProxy#build_model repeatedly would cause saving to happen #4058 [anna@wota.jp] + +* Added Sybase database adapter that relies on the Sybase Open Client bindings (see http://raa.ruby-lang.org/project/sybase-ctlib) #3765 [John Sheets]. It's almost completely Active Record compliant (including migrations), but has the following caveats: + + * Does not support DATE SQL column types; use DATETIME instead. + * Date columns on HABTM join tables are returned as String, not Time. + * Insertions are potentially broken for :polymorphic join tables + * BLOB column access not yet fully supported + +* Clear stale, cached connections left behind by defunct threads. [Jeremy Kemper] + +* CHANGED DEFAULT: set ActiveRecord::Base.allow_concurrency to false. Most AR usage is in single-threaded applications. [Jeremy Kemper] + +* Renamed the "oci" adapter to "oracle", but kept the old name as an alias #4017 [schoenm@earthlink.net] + +* Fixed that Base.save should always return false if the save didn't succeed, including if it has halted by before_save's #1861, #2477 [DHH] + +* Speed up class -> connection caching and stale connection verification. #3979 [Stefan Kaes] + +* Add set_fixture_class to allow the use of table name accessors with models which use set_table_name. [Kevin Clark] + +* Added that fixtures to placed in subdirectories of the main fixture files are also loaded #3937 [dblack@wobblini.net] + +* Define attribute query methods to avoid method_missing calls. #3677 [jonathan@bluewire.net.nz] + +* ActiveRecord::Base.remove_connection explicitly closes database connections and doesn't corrupt the connection cache. Introducing the disconnect! instance method for the PostgreSQL, MySQL, and SQL Server adapters; implementations for the others are welcome. #3591 [Simon Stapleton, Tom Ward] + +* Added support for nested scopes #3407 [anna@wota.jp]. Examples: + + Developer.with_scope(:find => { :conditions => "salary > 10000", :limit => 10 }) do + Developer.find(:all) # => SELECT * FROM developers WHERE (salary > 10000) LIMIT 10 + + # inner rule is used. (all previous parameters are ignored) + Developer.with_exclusive_scope(:find => { :conditions => "name = 'Jamis'" }) do + Developer.find(:all) # => SELECT * FROM developers WHERE (name = 'Jamis') + end + + # parameters are merged + Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do + Developer.find(:all) # => SELECT * FROM developers WHERE (( salary > 10000 ) AND ( name = 'Jamis' )) LIMIT 10 + end + end + +* Fixed db2 connection with empty user_name and auth options #3622 [phurley@gmail.com] + +* Fixed validates_length_of to work on UTF-8 strings by using characters instead of bytes #3699 [Masao Mutoh] + +* Fixed that reflections would bleed across class boundaries in single-table inheritance setups #3796 [lars@pind.com] + +* Added calculations: Base.count, Base.average, Base.sum, Base.minimum, Base.maxmium, and the generic Base.calculate. All can be used with :group and :having. Calculations and statitics need no longer require custom SQL. #3958 [Rick Olson]. Examples: + + Person.average :age + Person.minimum :age + Person.maximum :age + Person.sum :salary, :group => :last_name + +* Renamed Errors#count to Errors#size but kept an alias for the old name (and included an alias for length too) #3920 [contact@lukeredpath.co.uk] + +* Reflections don't attempt to resolve module nesting of association classes. Simplify type computation. [Jeremy Kemper] + +* Improved the Oracle OCI Adapter with better performance for column reflection (from #3210), fixes to migrations (from #3476 and #3742), tweaks to unit tests (from #3610), and improved documentation (from #2446) #3879 [Aggregated by schoenm@earthlink.net] + +* Fixed that the schema_info table used by ActiveRecord::Schema.define should respect table pre- and suffixes #3834 [rubyonrails@atyp.de] + +* Added :select option to Base.count that'll allow you to select something else than * to be counted on. Especially important for count queries using DISTINCT #3839 [skaes] + +* Correct syntax error in mysql DDL, and make AAACreateTablesTest run first [Bob Silva] + +* Allow :include to be used with has_many :through associations #3611 [Michael Schoen] + +* PostgreSQL: smarter schema dumps using pk_and_sequence_for(table). #2920 [Blair Zajac] + +* SQLServer: more compatible limit/offset emulation. #3779 [Tom Ward] + +* Polymorphic join support for has_one associations (has_one :foo, :as => :bar) #3785 [Rick Olson] + +* PostgreSQL: correctly parse negative integer column defaults. #3776 [bellis@deepthought.org] + +* Fix problems with count when used with :include [Jeremy Hopple and Kevin Clark] + +* ActiveRecord::RecordInvalid now states which validations failed in its default error message [Tobias Luetke] + +* Using AssociationCollection#build with arrays of hashes should call build, not create [DHH] + +* Remove definition of reloadable? from ActiveRecord::Base to make way for new Reloadable code. [Nicholas Seckar] + +* Fixed schema handling for DB2 adapter that didn't work: an initial schema could be set, but it wasn't used when getting tables and indexes #3678 [Maik Schmidt] + +* Support the :column option for remove_index with the PostgreSQL adapter. #3661 [shugo@ruby-lang.org] + +* Add documentation for add_index and remove_index. #3600 [Manfred Stienstra ] + +* If the OCI library is not available, raise an exception indicating as much. #3593 [schoenm@earthlink.net] + +* Add explicit :order in finder tests as postgresql orders results differently by default. #3577. [Rick Olson] + +* Make dynamic finders honor additional passed in :conditions. #3569 [Oleg Pudeyev , Marcel Molina Jr.] + +* Show a meaningful error when the DB2 adapter cannot be loaded due to missing dependencies. [Nicholas Seckar] + +* Make .count work for has_many associations with multi line finder sql [schoenm@earthlink.net] + +* Add AR::Base.base_class for querying the ancestor AR::Base subclass [Jamis Buck] + +* Allow configuration of the column used for optimistic locking [wilsonb@gmail.com] + +* Don't hardcode 'id' in acts as list. [ror@philippeapril.com] + +* Fix date errors for SQLServer in association tests. #3406 [kevin.clark@gmal.com] + +* Escape database name in MySQL adapter when creating and dropping databases. #3409 [anna@wota.jp] + +* Disambiguate table names for columns in validates_uniquness_of's WHERE clause. #3423 [alex.borovsky@gmail.com] + +* .with_scope imposed create parameters now bypass attr_protected [Tobias Luetke] + +* Don't raise an exception when there are more keys than there are named bind variables when sanitizing conditions. [Marcel Molina Jr.] + +* Multiple enhancements and adjustments to DB2 adaptor. #3377 [contact@maik-schmidt.de] + +* Sanitize scoped conditions. [Marcel Molina Jr.] + +* Added option to Base.reflection_of_all_associations to specify a specific association to scope the call. For example Base.reflection_of_all_associations(:has_many) [DHH] + +* Added ActiveRecord::SchemaDumper.ignore_tables which tells SchemaDumper which tables to ignore. Useful for tables with funky column like the ones required for tsearch2. [TobiasLuetke] + +* SchemaDumper now doesn't fail anymore when there are unknown column types in the schema. Instead the table is ignored and a Comment is left in the schema.rb. [TobiasLuetke] + +* Fixed that saving a model with multiple habtm associations would only save the first one. #3244 [yanowitz-rubyonrails@quantumfoam.org, Florian Weber] + +* Fix change_column to work with PostgreSQL 7.x and 8.x. #3141 [wejn@box.cz, Rick Olson, Scott Barron] + +* removed :piggyback in favor of just allowing :select on :through associations. [Tobias Luetke] + +* made method missing delegation to class methods on relation target work on :through associations. [Tobias Luetke] + +* made .find() work on :through relations. [Tobias Luetke] + +* Fix typo in association docs. #3296. [Blair Zajac] + +* Fixed :through relations when using STI inherited classes would use the inherited class's name as foreign key on the join model [Tobias Luetke] + + +*1.13.2* (December 13th, 2005) + +* Become part of Rails 1.0 + +* MySQL: allow encoding option for mysql.rb driver. [Jeremy Kemper] + +* Added option inheritance for find calls on has_and_belongs_to_many and has_many assosociations [DHH]. Example: + + class Post + has_many :recent_comments, :class_name => "Comment", :limit => 10, :include => :author + end + + post.recent_comments.find(:all) # Uses LIMIT 10 and includes authors + post.recent_comments.find(:all, :limit => nil) # Uses no limit but include authors + post.recent_comments.find(:all, :limit => nil, :include => nil) # Uses no limit and doesn't include authors + +* Added option to specify :group, :limit, :offset, and :select options from find on has_and_belongs_to_many and has_many assosociations [DHH] + +* MySQL: fixes for the bundled mysql.rb driver. #3160 [Justin Forder] + +* SQLServer: fix obscure optimistic locking bug. #3068 [kajism@yahoo.com] + +* SQLServer: support uniqueidentifier columns. #2930 [keithm@infused.org] + +* SQLServer: cope with tables names qualified by owner. #3067 [jeff@ministrycentered.com] + +* SQLServer: cope with columns with "desc" in the name. #1950 [Ron Lusk, Ryan Tomayko] + +* SQLServer: cope with primary keys with "select" in the name. #3057 [rdifrango@captechventures.com] + +* Oracle: active? performs a select instead of a commit. #3133 [Michael Schoen] + +* MySQL: more robust test for nullified result hashes. #3124 [Stefan Kaes] + +* Reloading an instance refreshes its aggregations as well as its associations. #3024 [François Beausolei] + +* Fixed that using :include together with :conditions array in Base.find would cause NoMethodError #2887 [Paul Hammmond] + +* PostgreSQL: more robust sequence name discovery. #3087 [Rick Olson] + +* Oracle: use syntax compatible with Oracle 8. #3131 [Michael Schoen] + +* MySQL: work around ruby-mysql/mysql-ruby inconsistency with mysql.stat. Eliminate usage of mysql.ping because it doesn't guarantee reconnect. Explicitly close and reopen the connection instead. [Jeremy Kemper] + +* Added preliminary support for polymorphic associations [DHH] + +* Added preliminary support for join models [DHH] + +* Allow validate_uniqueness_of to be scoped by more than just one column. #1559. [jeremy@jthopple.com, Marcel Molina Jr.] + +* Firebird: active? and reconnect! methods for handling stale connections. #428 [Ken Kunz ] + +* Firebird: updated for FireRuby 0.4.0. #3009 [Ken Kunz ] + +* MySQL and PostgreSQL: active? compatibility with the pure-Ruby driver. #428 [Jeremy Kemper] + +* Oracle: active? check pings the database rather than testing the last command status. #428 [Michael Schoen] + +* SQLServer: resolve column aliasing/quoting collision when using limit or offset in an eager find. #2974 [kajism@yahoo.com] + +* Reloading a model doesn't lose track of its connection. #2996 [junk@miriamtech.com, Jeremy Kemper] + +* Fixed bug where using update_attribute after pushing a record to a habtm association of the object caused duplicate rows in the join table. #2888 [colman@rominato.com, Florian Weber, Michael Schoen] + +* MySQL, PostgreSQL: reconnect! also reconfigures the connection. Otherwise, the connection 'loses' its settings if it times out and is reconnected. #2978 [Shugo Maeda] + +* has_and_belongs_to_many: use JOIN instead of LEFT JOIN. [Jeremy Kemper] + +* MySQL: introduce :encoding option to specify the character set for client, connection, and results. Only available for MySQL 4.1 and later with the mysql-ruby driver. Do SHOW CHARACTER SET in mysql client to see available encodings. #2975 [Shugo Maeda] + +* Add tasks to create, drop and rebuild the MySQL and PostgreSQL test databases. [Marcel Molina Jr.] + +* Correct boolean handling in generated reader methods. #2945 [don.park@gmail.com, Stefan Kaes] + +* Don't generate read methods for columns whose names are not valid ruby method names. #2946 [Stefan Kaes] + +* Document :force option to create_table. #2921 [Blair Zajac ] + +* Don't add the same conditions twice in has_one finder sql. #2916 [Jeremy Evans] + +* Rename Version constant to VERSION. #2802 [Marcel Molina Jr.] + +* Introducing the Firebird adapter. Quote columns and use attribute_condition more consistently. Setup guide: http://wiki.rubyonrails.com/rails/pages/Firebird+Adapter #1874 [Ken Kunz ] + +* SQLServer: active? and reconnect! methods for handling stale connections. #428 [kajism@yahoo.com, Tom Ward ] + +* Associations handle case-equality more consistently: item.parts.is_a?(Array) and item.parts === Array. #1345 [MarkusQ@reality.com] + +* SQLServer: insert uses given primary key value if not nil rather than SELECT @@IDENTITY. #2866 [kajism@yahoo.com, Tom Ward ] + +* Oracle: active? and reconnect! methods for handling stale connections. Optionally retry queries after reconnect. #428 [Michael Schoen ] + +* Correct documentation for Base.delete_all. #1568 [Newhydra] + +* Oracle: test case for column default parsing. #2788 [Michael Schoen ] + +* Update documentation for Migrations. #2861 [Tom Werner ] + +* When AbstractAdapter#log rescues an exception, attempt to detect and reconnect to an inactive database connection. Connection adapter must respond to the active? and reconnect! instance methods. Initial support for PostgreSQL, MySQL, and SQLite. Make certain that all statements which may need reconnection are performed within a logged block: for example, this means no avoiding log(sql, name) { } if @logger.nil? #428 [Jeremy Kemper] + +* Oracle: Much faster column reflection. #2848 [Michael Schoen ] + +* Base.reset_sequence_name analogous to reset_table_name (mostly useful for testing). Base.define_attr_method allows nil values. [Jeremy Kemper] + +* PostgreSQL: smarter sequence name defaults, stricter last_insert_id, warn on pk without sequence. [Jeremy Kemper] + +* PostgreSQL: correctly discover custom primary key sequences. #2594 [Blair Zajac , meadow.nnick@gmail.com, Jeremy Kemper] + +* SQLServer: don't report limits for unsupported field types. #2835 [Ryan Tomayko] + +* Include the Enumerable module in ActiveRecord::Errors. [Rick Bradley ] + +* Add :group option, correspond to GROUP BY, to the find method and to the has_many association. #2818 [rubyonrails@atyp.de] + +* Don't cast nil or empty strings to a dummy date. #2789 [Rick Bradley ] + +* acts_as_list plays nicely with inheritance by remembering the class which declared it. #2811 [rephorm@rephorm.com] + +* Fix sqlite adaptor's detection of missing dbfile or database declaration. [Nicholas Seckar] + +* Fixed acts_as_list for definitions without an explicit :order #2803 [jonathan@bluewire.net.nz] + +* Upgrade bundled ruby-mysql 0.2.4 with mysql411 shim (see #440) to ruby-mysql 0.2.6 with a patchset for 4.1 protocol support. Local change [301] is now a part of the main driver; reapplied local change [2182]. Removed GC.start from Result.free. [tommy@tmtm.org, akuroda@gmail.com, Doug Fales , Jeremy Kemper] + +* Correct handling of complex order clauses with SQL Server limit emulation. #2770 [Tom Ward , Matt B.] + +* Correct whitespace problem in Oracle default column value parsing. #2788 [rick@rickbradley.com] + +* Destroy associated has_and_belongs_to_many records after all before_destroy callbacks but before destroy. This allows you to act on the habtm association as you please while preserving referential integrity. #2065 [larrywilliams1@gmail.com, sam.kirchmeier@gmail.com, elliot@townx.org, Jeremy Kemper] + +* Deprecate the old, confusing :exclusively_dependent option in favor of :dependent => :delete_all. [Jeremy Kemper] + +* More compatible Oracle column reflection. #2771 [Ryan Davis , Michael Schoen ] + + +*1.13.0* (November 7th, 2005) + +* Fixed faulty regex in get_table_name method (SQLServerAdapter) #2639 [Ryan Tomayko] + +* Added :include as an option for association declarations [DHH]. Example: + + has_many :posts, :include => [ :author, :comments ] + +* Rename Base.constrain to Base.with_scope so it doesn't conflict with existing concept of database constraints. Make scoping more robust: uniform method => parameters, validated method names and supported finder parameters, raise exception on nested scopes. [Jeremy Kemper] Example: + + Comment.with_scope(:find => { :conditions => 'active=true' }, :create => { :post_id => 5 }) do + # Find where name = ? and active=true + Comment.find :all, :conditions => ['name = ?', name] + # Create comment associated with :post_id + Comment.create :body => "Hello world" + end + +* Fixed that SQL Server should ignore :size declarations on anything but integer and string in the agnostic schema representation #2756 [Ryan Tomayko] + +* Added constrain scoping for creates using a hash of attributes bound to the :creation key [DHH]. Example: + + Comment.constrain(:creation => { :post_id => 5 }) do + # Associated with :post_id + Comment.create :body => "Hello world" + end + + This is rarely used directly, but allows for find_or_create on associations. So you can do: + + # If the tag doesn't exist, a new one is created that's associated with the person + person.tags.find_or_create_by_name("Summer") + +* Added find_or_create_by_X as a second type of dynamic finder that'll create the record if it doesn't already exist [DHH]. Example: + + # No 'Summer' tag exists + Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer") + + # Now the 'Summer' tag does exist + Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer") + +* Added extension capabilities to has_many and has_and_belongs_to_many proxies [DHH]. Example: + + class Account < ActiveRecord::Base + has_many :people do + def find_or_create_by_name(name) + first_name, *last_name = name.split + last_name = last_name.join " " + + find_or_create_by_first_name_and_last_name(first_name, last_name) + end + end + end + + person = Account.find(:first).people.find_or_create_by_name("David Heinemeier Hansson") + person.first_name # => "David" + person.last_name # => "Heinemeier Hansson" + + Note that the anoymous module must be declared using brackets, not do/end (due to order of evaluation). + +* Omit internal dtproperties table from SQLServer table list. #2729 [rtomayko@gmail.com] + +* Quote column names in generated SQL. #2728 [rtomayko@gmail.com] + +* Correct the pure-Ruby MySQL 4.1.1 shim's version test. #2718 [Jeremy Kemper] + +* Add Model.create! to match existing model.save! method. When save! raises RecordInvalid, you can catch the exception, retrieve the invalid record (invalid_exception.record), and see its errors (invalid_exception.record.errors). [Jeremy Kemper] + +* Correct fixture behavior when table name pluralization is off. #2719 [Rick Bradley ] + +* Changed :dbfile to :database for SQLite adapter for consistency (old key still works as an alias) #2644 [Dan Peterson] + +* Added migration support for Oracle #2647 [Michael Schoen] + +* Worked around that connection can't be reset if allow_concurrency is off. #2648 [Michael Schoen ] + +* Fixed SQL Server adapter to pass even more tests and do even better #2634 [rtomayko@gmail.com] + +* Fixed SQL Server adapter so it honors options[:conditions] when applying :limits #1978 [Tom Ward] + +* Added migration support to SQL Server adapter (please someone do the same for Oracle and DB2) #2625 [Tom Ward] + +* Use AR::Base.silence rather than AR::Base.logger.silence in fixtures to preserve Log4r compatibility. #2618 [dansketcher@gmail.com] + +* Constraints are cloned so they can't be inadvertently modified while they're +in effect. Added :readonly finder constraint. Calling an association collection's class method (Part.foobar via item.parts.foobar) constrains :readonly => false since the collection's :joins constraint would otherwise force it to true. [Jeremy Kemper ] + +* Added :offset and :limit to the kinds of options that Base.constrain can use #2466 [duane.johnson@gmail.com] + +* Fixed handling of nil number columns on Oracle and cleaned up tests for Oracle in general #2555 [schoenm@earthlink.net] + +* Added quoted_true and quoted_false methods and tables to db2_adapter and cleaned up tests for DB2 #2493, #2624 [maik schmidt] + + +*1.12.2* (October 26th, 2005) + +* Allow symbols to rename columns when using SQLite adapter. #2531 [kevin.clark@gmail.com] + +* Map Active Record time to SQL TIME. #2575, #2576 [Robby Russell ] + +* Clarify semantics of ActiveRecord::Base#respond_to? #2560 [skaes@web.de] + +* Fixed Association#clear for associations which have not yet been accessed. #2524 [Patrick Lenz ] + +* HABTM finders shouldn't return readonly records. #2525 [Patrick Lenz ] + +* Make all tests runnable on their own. #2521. [Blair Zajac ] + + +*1.12.1* (October 19th, 2005) + +* Always parenthesize :conditions options so they may be safely combined with STI and constraints. + +* Correct PostgreSQL primary key sequence detection. #2507 [tmornini@infomania.com] + +* Added support for using limits in eager loads that involve has_many and has_and_belongs_to_many associations + + +*1.12.0* (October 16th, 2005) + +* Update/clean up documentation (rdoc) + +* PostgreSQL sequence support. Use set_sequence_name in your model class to specify its primary key sequence. #2292 [Rick Olson , Robby Russell ] + +* Change default logging colors to work on both white and black backgrounds. [Sam Stephenson] + +* YAML fixtures support ordered hashes for fixtures with foreign key dependencies in the same table. #1896 [purestorm@ggnore.net] + +* :dependent now accepts :nullify option. Sets the foreign key of the related objects to NULL instead of deleting them. #2015 [Robby Russell ] + +* Introduce read-only records. If you call object.readonly! then it will mark the object as read-only and raise ReadOnlyRecord if you call object.save. object.readonly? reports whether the object is read-only. Passing :readonly => true to any finder method will mark returned records as read-only. The :joins option now implies :readonly, so if you use this option, saving the same record will now fail. Use find_by_sql to work around. + +* Avoid memleak in dev mode when using fcgi + +* Simplified .clear on active record associations by using the existing delete_records method. #1906 [Caleb ] + +* Delegate access to a customized primary key to the conventional id method. #2444. [Blair Zajac ] + +* Fix errors caused by assigning a has-one or belongs-to property to itself + +* Add ActiveRecord::Base.schema_format setting which specifies how databases should be dumped [Sam Stephenson] + +* Update DB2 adapter. #2206. [contact@maik-schmidt.de] + +* Corrections to SQLServer native data types. #2267. [rails.20.clarry@spamgourmet.com] + +* Deprecated ActiveRecord::Base.threaded_connection in favor of ActiveRecord::Base.allow_concurrency. + +* Protect id attribute from mass assigment even when the primary key is set to something else. #2438. [Blair Zajac ] + +* Misc doc fixes (typos/grammar/etc.). #2430. [coffee2code] + +* Add test coverage for content_columns. #2432. [coffee2code] + +* Speed up for unthreaded environments. #2431. [skaes@web.de] + +* Optimization for Mysql selects using mysql-ruby extension greater than 2.6.3. #2426. [skaes@web.de] + +* Speed up the setting of table_name. #2428. [skaes@web.de] + +* Optimize instantiation of STI subclass records. In partial fullfilment of #1236. [skaes@web.de] + +* Fix typo of 'constrains' to 'contraints'. #2069. [Michael Schuerig ] + +* Optimization refactoring for add_limit_offset!. In partial fullfilment of #1236. [skaes@web.de] + +* Add ability to get all siblings, including the current child, with acts_as_tree. Recloses #2140. [Michael Schuerig ] + +* Add geometric type for postgresql adapter. #2233 [akaspick@gmail.com] + +* Add option (true by default) to generate reader methods for each attribute of a record to avoid the overhead of calling method missing. In partial fullfilment of #1236. [skaes@web.de] + +* Add convenience predicate methods on Column class. In partial fullfilment of #1236. [skaes@web.de] + +* Raise errors when invalid hash keys are passed to ActiveRecord::Base.find. #2363 [Chad Fowler , Nicholas Seckar] + +* Added :force option to create_table that'll try to drop the table if it already exists before creating + +* Fix transactions so that calling return while inside a transaction will not leave an open transaction on the connection. [Nicholas Seckar] + +* Use foreign_key inflection uniformly. #2156 [Blair Zajac ] + +* model.association.clear should destroy associated objects if :dependent => true instead of nullifying their foreign keys. #2221 [joergd@pobox.com, ObieFernandez ] + +* Returning false from before_destroy should cancel the action. #1829 [Jeremy Huffman] + +* Recognize PostgreSQL NOW() default as equivalent to CURRENT_TIMESTAMP or CURRENT_DATE, depending on the column's type. #2256 [mat ] + +* Extensive documentation for the abstract database adapter. #2250 [François Beausoleil ] + +* Clean up Fixtures.reset_sequences for PostgreSQL. Handle tables with no rows and models with custom primary keys. #2174, #2183 [jay@jay.fm, Blair Zajac ] + +* Improve error message when nil is assigned to an attr which validates_size_of within a range. #2022 [Manuel Holtgrewe ] + +* Make update_attribute use the same writer method that update_attributes uses. + #2237 [trevor@protocool.com] + +* Make migrations honor table name prefixes and suffixes. #2298 [Jakob S, Marcel Molina] + +* Correct and optimize PostgreSQL bytea escaping. #1745, #1837 [dave@cherryville.org, ken@miriamtech.com, bellis@deepthought.org] + +* Fixtures should only reset a PostgreSQL sequence if it corresponds to an integer primary key named id. #1749 [chris@chrisbrinker.com] + +* Standardize the interpretation of boolean columns in the Mysql and Sqlite adapters. (Use MysqlAdapter.emulate_booleans = false to disable this behavior) + +* Added new symbol-driven approach to activating observers with Base#observers= [DHH]. Example: + + ActiveRecord::Base.observers = :cacher, :garbage_collector + +* Added AbstractAdapter#select_value and AbstractAdapter#select_values as convenience methods for selecting single values, instead of hashes, of the first column in a SELECT #2283 [solo@gatelys.com] + +* Wrap :conditions in parentheses to prevent problems with OR's #1871 [Jamis Buck] + +* Allow the postgresql adapter to work with the SchemaDumper. [Jamis Buck] + +* Add ActiveRecord::SchemaDumper for dumping a DB schema to a pure-ruby file, making it easier to consolidate large migration lists and port database schemas between databases. [Jamis Buck] + +* Fixed migrations for Windows when using more than 10 [David Naseby] + +* Fixed that the create_x method from belongs_to wouldn't save the association properly #2042 [Florian Weber] + +* Fixed saving a record with two unsaved belongs_to associations pointing to the same object #2023 [Tobias Luetke] + +* Improved migrations' behavior when the schema_info table is empty. [Nicholas Seckar] + +* Fixed that Observers didn't observe sub-classes #627 [Florian Weber] + +* Fix eager loading error messages, allow :include to specify tables using strings or symbols. Closes #2222 [Marcel Molina] + +* Added check for RAILS_CONNECTION_ADAPTERS on startup and only load the connection adapters specified within if its present (available in Rails through config.connection_adapters using the new config) #1958 [skae] + +* Fixed various problems with has_and_belongs_to_many when using customer finder_sql #2094 [Florian Weber] + +* Added better exception error when unknown column types are used with migrations #1814 [fbeausoleil@ftml.net] + +* Fixed "connection lost" issue with the bundled Ruby/MySQL driver (would kill the app after 8 hours of inactivity) #2163, #428 [kajism@yahoo.com] + +* Fixed comparison of Active Record objects so two new objects are not equal #2099 [deberg] + +* Fixed that the SQL Server adapter would sometimes return DBI::Timestamp objects instead of Time #2127 [Tom Ward] + +* Added the instance methods #root and #ancestors on acts_as_tree and fixed siblings to not include the current node #2142, #2140 [coffee2code] + +* Fixed that Active Record would call SHOW FIELDS twice (or more) for the same model when the cached results were available #1947 [sd@notso.net] + +* Added log_level and use_silence parameter to ActiveRecord::Base.benchmark. The first controls at what level the benchmark statement will be logged (now as debug, instead of info) and the second that can be passed false to include all logging statements during the benchmark block/ + +* Make sure the schema_info table is created before querying the current version #1903 + +* Fixtures ignore table name prefix and suffix #1987 [Jakob S] + +* Add documentation for index_type argument to add_index method for migrations #2005 [blaine@odeo.com] + +* Modify read_attribute to allow a symbol argument #2024 [Ken Kunz] + +* Make destroy return self #1913 [sebastian.kanthak@muehlheim.de] + +* Fix typo in validations documentation #1938 [court3nay] + +* Make acts_as_list work for insert_at(1) #1966 [hensleyl@papermountain.org] + +* Fix typo in count_by_sql documentation #1969 [Alexey Verkhovsky] + +* Allow add_column and create_table to specify NOT NULL #1712 [emptysands@gmail.com] + +* Fix create_table so that id column is implicitly added [Rick Olson] + +* Default sequence names for Oracle changed to #{table_name}_seq, which is the most commonly used standard. In addition, a new method ActiveRecord::Base#set_sequence_name allows the developer to set the sequence name per model. This is a non-backwards-compatible change -- anyone using the old-style "rails_sequence" will need to either create new sequences, or set: ActiveRecord::Base.set_sequence_name = "rails_sequence" #1798 + +* OCIAdapter now properly handles synonyms, which are commonly used to separate out the schema owner from the application user #1798 + +* Fixed the handling of camelCase columns names in Oracle #1798 + +* Implemented for OCI the Rakefile tasks of :clone_structure_to_test, :db_structure_dump, and :purge_test_database, which enable Oracle folks to enjoy all the agile goodness of Rails for testing. Note that the current implementation is fairly limited -- only tables and sequences are cloned, not constraints or indexes. A full clone in Oracle generally requires some manual effort, and is version-specific. Post 9i, Oracle recommends the use of the DBMS_METADATA package, though that approach requires editing of the physical characteristics generated #1798 + +* Fixed the handling of multiple blob columns in Oracle if one or more of them are null #1798 + +* Added support for calling constrained class methods on has_many and has_and_belongs_to_many collections #1764 [Tobias Luetke] + + class Comment < AR:B + def self.search(q) + find(:all, :conditions => ["body = ?", q]) + end + end + + class Post < AR:B + has_many :comments + end + + Post.find(1).comments.search('hi') # => SELECT * from comments WHERE post_id = 1 AND body = 'hi' + + NOTICE: This patch changes the underlying SQL generated by has_and_belongs_to_many queries. If your relying on that, such as + by explicitly referencing the old t and j aliases, you'll need to update your code. Of course, you _shouldn't_ be relying on + details like that no less than you should be diving in to touch private variables. But just in case you do, consider yourself + noticed :) + +* Added migration support for SQLite (using temporary tables to simulate ALTER TABLE) #1771 [Sam Stephenson] + +* Remove extra definition of supports_migrations? from abstract_adaptor.rb [Nicholas Seckar] + +* Fix acts_as_list so that moving next-to-last item to the bottom does not result in duplicate item positions + +* Fixed incompatibility in DB2 adapter with the new limit/offset approach #1718 [Maik Schmidt] + +* Added :select option to find which can specify a different value than the default *, like find(:all, :select => "first_name, last_name"), if you either only want to select part of the columns or exclude columns otherwise included from a join #1338 [Stefan Kaes] + + +*1.11.1* (11 July, 2005) + +* Added support for limit and offset with eager loading of has_one and belongs_to associations. Using the options with has_many and has_and_belongs_to_many associations will now raise an ActiveRecord::ConfigurationError #1692 [Rick Olsen] + +* Fixed that assume_bottom_position (in acts_as_list) could be called on items already last in the list and they would move one position away from the list #1648 [tyler@kianta.com] + +* Added ActiveRecord::Base.threaded_connections flag to turn off 1-connection per thread (required for thread safety). By default it's on, but WEBrick in Rails need it off #1685 [Sam Stephenson] + +* Correct reflected table name for singular associations. #1688 [court3nay@gmail.com] + +* Fixed optimistic locking with SQL Server #1660 [tom@popdog.net] + +* Added ActiveRecord::Migrator.migrate that can figure out whether to go up or down based on the target version and the current + +* Added better error message for "packets out of order" #1630 [courtenay] + +* Fixed first run of "rake migrate" on PostgreSQL by not expecting a return value on the id #1640 + + +*1.11.0* (6 July, 2005) + +* Fixed that Yaml error message in fixtures hid the real error #1623 [Nicholas Seckar] + +* Changed logging of SQL statements to use the DEBUG level instead of INFO + +* Added new Migrations framework for describing schema transformations in a way that can be easily applied across multiple databases #1604 [Tobias Luetke] See documentation under ActiveRecord::Migration and the additional support in the Rails rakefile/generator. + +* Added callback hooks to association collections #1549 [Florian Weber]. Example: + + class Project + has_and_belongs_to_many :developers, :before_add => :evaluate_velocity + + def evaluate_velocity(developer) + ... + end + end + + ..raising an exception will cause the object not to be added (or removed, with before_remove). + + +* Fixed Base.content_columns call for SQL Server adapter #1450 [DeLynn Berry] + +* Fixed Base#write_attribute to work with both symbols and strings #1190 [Paul Legato] + +* Fixed that has_and_belongs_to_many didn't respect single table inheritance types #1081 [Florian Weber] + +* Speed up ActiveRecord#method_missing for the common case (read_attribute). + +* Only notify observers on after_find and after_initialize if these methods are defined on the model. #1235 [skaes@web.de] + +* Fixed that single-table inheritance sub-classes couldn't be used to limit the result set with eager loading #1215 [Chris McGrath] + +* Fixed validates_numericality_of to work with overrided getter-method when :allow_nil is on #1316 [raidel@onemail.at] + +* Added roots, root, and siblings to the batch of methods added by acts_as_tree #1541 [michael@schuerig.de] + +* Added support for limit/offset with the MS SQL Server driver so that pagination will now work #1569 [DeLynn Berry] + +* Added support for ODBC connections to MS SQL Server so you can connect from a non-Windows machine #1569 [Mark Imbriaco/DeLynn Berry] + +* Fixed that multiparameter posts ignored attr_protected #1532 [alec+rails@veryclever.net] + +* Fixed problem with eager loading when using a has_and_belongs_to_many association using :association_foreign_key #1504 [flash@vanklinkenbergsoftware.nl] + +* Fixed Base#find to honor the documentation on how :joins work and make them consistent with Base#count #1405 [pritchie@gmail.com]. What used to be: + + Developer.find :all, :joins => 'developers_projects', :conditions => 'id=developer_id AND project_id=1' + + ...should instead be: + + Developer.find( + :all, + :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', + :conditions => 'project_id=1' + ) + +* Fixed that validations didn't respecting custom setting for too_short, too_long messages #1437 [Marcel Molina] + +* Fixed that clear_association_cache doesn't delete new associations on new records (so you can safely place new records in the session with Action Pack without having new associations wiped) #1494 [cluon] + +* Fixed that calling Model.find([]) returns [] and doesn't throw an exception #1379 + +* Fixed that adding a record to a has_and_belongs_to collection would always save it -- now it only saves if its a new record #1203 [Alisdair McDiarmid] + +* Fixed saving of in-memory association structures to happen as a after_create/after_update callback instead of after_save -- that way you can add new associations in after_create/after_update callbacks without getting them saved twice + +* Allow any Enumerable, not just Array, to work as bind variables #1344 [Jeremy Kemper] + +* Added actual database-changing behavior to collection assigment for has_many and has_and_belongs_to_many #1425 [Sebastian Kanthak]. + Example: + + david.projects = [Project.find(1), Project.new("name" => "ActionWebSearch")] + david.save + + If david.projects already contain the project with ID 1, this is left unchanged. Any other projects are dropped. And the new + project is saved when david.save is called. + + Also included is a way to do assignments through IDs, which is perfect for checkbox updating, so you get to do: + + david.project_ids = [1, 5, 7] + +* Corrected typo in find SQL for has_and_belongs_to_many. #1312 [ben@bensinclair.com] + +* Fixed sanitized conditions for has_many finder method. #1281 [jackc@hylesanderson.com, pragdave, Tobias Luetke] + +* Comprehensive PostgreSQL schema support. Use the optional schema_search_path directive in database.yml to give a comma-separated list of schemas to search for your tables. This allows you, for example, to have tables in a shared schema without having to use a custom table name. See http://www.postgresql.org/docs/8.0/interactive/ddl-schemas.html to learn more. #827 [dave@cherryville.org] + +* Corrected @@configurations typo #1410 [david@ruppconsulting.com] + +* Return PostgreSQL columns in the order they were declared #1374 [perlguy@gmail.com] + +* Allow before/after update hooks to work on models using optimistic locking + +* Eager loading of dependent has_one associations won't delete the association #1212 + +* Added a second parameter to the build and create method for has_one that controls whether the existing association should be replaced (which means nullifying its foreign key as well). By default this is true, but false can be passed to prevent it. + +* Using transactional fixtures now causes the data to be loaded only once. + +* Added fixture accessor methods that can be used when instantiated fixtures are disabled. + + fixtures :web_sites + + def test_something + assert_equal "Ruby on Rails", web_sites(:rubyonrails).name + end + +* Added DoubleRenderError exception that'll be raised if render* is called twice #518 [Nicholas Seckar] + +* Fixed exceptions occuring after render has been called #1096 [Nicholas Seckar] + +* CHANGED: validates_presence_of now uses Errors#add_on_blank, which will make " " fail the validation where it didn't before #1309 + +* Added Errors#add_on_blank which works like Errors#add_on_empty, but uses Object#blank? instead + +* Added the :if option to all validations that can either use a block or a method pointer to determine whether the validation should be run or not. #1324 [Duane Johnson/jhosteny]. Examples: + + Conditional validations such as the following are made possible: + validates_numericality_of :income, :if => :employed? + + Conditional validations can also solve the salted login generator problem: + validates_confirmation_of :password, :if => :new_password? + + Using blocks: + validates_presence_of :username, :if => Proc.new { |user| user.signup_step > 1 } + +* Fixed use of construct_finder_sql when using :join #1288 [dwlt@dwlt.net] + +* Fixed that :delete_sql in has_and_belongs_to_many associations couldn't access record properties #1299 [Rick Olson] + +* Fixed that clone would break when an aggregate had the same name as one of its attributes #1307 [Jeremy Kemper] + +* Changed that destroying an object will only freeze the attributes hash, which keeps the object from having attributes changed (as that wouldn't make sense), but allows for the querying of associations after it has been destroyed. + +* Changed the callbacks such that observers are notified before the in-object callbacks are triggered. Without this change, it wasn't possible to act on the whole object in something like a before_destroy observer without having the objects own callbacks (like deleting associations) called first. + +* Added option for passing an array to the find_all version of the dynamic finders and have it evaluated as an IN fragment. Example: + + # SELECT * FROM topics WHERE title IN ('First', 'Second') + Topic.find_all_by_title(["First", "Second"]) + +* Added compatibility with camelCase column names for dynamic finders #533 [Dee.Zsombor] + +* Fixed extraneous comma in count() function that made it not work with joins #1156 [jarkko/Dee.Zsombor] + +* Fixed incompatibility with Base#find with an array of ids that would fail when using eager loading #1186 [Alisdair McDiarmid] + +* Fixed that validate_length_of lost :on option when :within was specified #1195 [jhosteny@mac.com] + +* Added encoding and min_messages options for PostgreSQL #1205 [shugo]. Configuration example: + + development: + adapter: postgresql + database: rails_development + host: localhost + username: postgres + password: + encoding: UTF8 + min_messages: ERROR + +* Fixed acts_as_list where deleting an item that was removed from the list would ruin the positioning of other list items #1197 [Jamis Buck] + +* Added validates_exclusion_of as a negative of validates_inclusion_of + +* Optimized counting of has_many associations by setting the association to empty if the count is 0 so repeated calls doesn't trigger database calls + + +*1.10.1* (20th April, 2005) + +* Fixed frivilous database queries being triggered with eager loading on empty associations and other things + +* Fixed order of loading in eager associations + +* Fixed stray comma when using eager loading and ordering together from has_many associations #1143 + + +*1.10.0* (19th April, 2005) + +* Added eager loading of associations as a way to solve the N+1 problem more gracefully without piggy-back queries. Example: + + for post in Post.find(:all, :limit => 100) + puts "Post: " + post.title + puts "Written by: " + post.author.name + puts "Last comment on: " + post.comments.first.created_on + end + + This used to generate 301 database queries if all 100 posts had both author and comments. It can now be written as: + + for post in Post.find(:all, :limit => 100, :include => [ :author, :comments ]) + + ...and the number of database queries needed is now 1. + +* Added new unified Base.find API and deprecated the use of find_first and find_all. See the documentation for Base.find. Examples: + + Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC") + Person.find(1, 5, 6, :conditions => "administrator = 1", :order => "created_on DESC") + Person.find(:first, :order => "created_on DESC", :offset => 5) + Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50) + Person.find(:all, :offset => 10, :limit => 10) + +* Added acts_as_nested_set #1000 [wschenk]. Introduction: + + This acts provides Nested Set functionality. Nested Set is similiar to Tree, but with + the added feature that you can select the children and all of it's descendants with + a single query. A good use case for this is a threaded post system, where you want + to display every reply to a comment without multiple selects. + +* Added Base.save! that attempts to save the record just like Base.save but will raise a RecordInvalid exception instead of returning false if the record is not valid [After much pestering from Dave Thomas] + +* Fixed PostgreSQL usage of fixtures with regards to public schemas and table names with dots #962 [gnuman1@gmail.com] + +* Fixed that fixtures were being deleted in the same order as inserts causing FK errors #890 [andrew.john.peters@gmail.com] + +* Fixed loading of fixtures in to be in the right order (or PostgreSQL would bark) #1047 [stephenh@chase3000.com] + +* Fixed page caching for non-vhost applications living underneath the root #1004 [Ben Schumacher] + +* Fixes a problem with the SQL Adapter which was resulting in IDENTITY_INSERT not being set to ON when it should be #1104 [adelle] + +* Added the option to specify the acceptance string in validates_acceptance_of #1106 [caleb@aei-tech.com] + +* Added insert_at(position) to acts_as_list #1083 [DeLynnB] + +* Removed the default order by id on has_and_belongs_to_many queries as it could kill performance on large sets (you can still specify by hand with :order) + +* Fixed that Base.silence should restore the old logger level when done, not just set it to DEBUG #1084 [yon@milliped.com] + +* Fixed boolean saving on Oracle #1093 [mparrish@pearware.org] + +* Moved build_association and create_association for has_one and belongs_to out of deprecation as they work when the association is nil unlike association.build and association.create, which require the association to be already in place #864 + +* Added rollbacks of transactions if they're active as the dispatcher is killed gracefully (TERM signal) #1054 [Leon Bredt] + +* Added quoting of column names for fixtures #997 [jcfischer@gmail.com] + +* Fixed counter_sql when no records exist in database for PostgreSQL (would give error, not 0) #1039 [Caleb Tennis] + +* Fixed that benchmarking times for rendering included db runtimes #987 [skaes@web.de] + +* Fixed boolean queries for t/f fields in PostgreSQL #995 [dave@cherryville.org] + +* Added that model.items.delete(child) will delete the child, not just set the foreign key to nil, if the child is dependent on the model #978 [Jeremy Kemper] + +* Fixed auto-stamping of dates (created_on/updated_on) for PostgreSQL #985 [dave@cherryville.org] + +* Fixed Base.silence/benchmark to only log if a logger has been configured #986 [skaes@web.de] + +* Added a join parameter as the third argument to Base.find_first and as the second to Base.count #426, #988 [skaes@web.de] + +* Fixed bug in Base#hash method that would treat records with the same string-based id as different [Dave Thomas] + +* Renamed DateHelper#distance_of_time_in_words_to_now to DateHelper#time_ago_in_words (old method name is still available as a deprecated alias) + + +*1.9.1* (27th March, 2005) + +* Fixed that Active Record objects with float attribute could not be cloned #808 + +* Fixed that MissingSourceFile's wasn't properly detected in production mode #925 [Nicholas Seckar] + +* Fixed that :counter_cache option would look for a line_items_count column for a LineItem object instead of lineitems_count + +* Fixed that AR exists?() would explode on postgresql if the passed id did not match the PK type #900 [Scott Barron] + +* Fixed the MS SQL adapter to work with the new limit/offset approach and with binary data (still suffering from 7KB limit, though) #901 [delynnb] + + +*1.9.0* (22th March, 2005) + +* Added adapter independent limit clause as a two-element array with the first being the limit, the second being the offset #795 [Sam Stephenson]. Example: + + Developer.find_all nil, 'id ASC', 5 # return the first five developers + Developer.find_all nil, 'id ASC', [3, 8] # return three developers, starting from #8 and forward + + This doesn't yet work with the DB2 or MS SQL adapters. Patches to make that happen are encouraged. + +* Added alias_method :to_param, :id to Base, such that Active Record objects to be used as URL parameters in Action Pack automatically #812 [Nicholas Seckar/Sam Stephenson] + +* Improved the performance of the OCI8 adapter for Oracle #723 [pilx/gjenkins] + +* Added type conversion before saving a record, so string-based values like "10.0" aren't left for the database to convert #820 [dave@cherryville.org] + +* Added with additional settings for working with transactional fixtures and pre-loaded test databases #865 [mindel] + +* Fixed acts_as_list to trigger remove_from_list on destroy after the fact, not before, so a unique position can be maintained #871 [Alisdair McDiarmid] + +* Added the possibility of specifying fixtures in multiple calls #816 [kim@tinker.com] + +* Added Base.exists?(id) that'll return true if an object of the class with the given id exists #854 [stian@grytoyr.net] + +* Added optionally allow for nil or empty strings with validates_numericality_of #801 [Sebastian Kanthak] + +* Fixed problem with using slashes in validates_format_of regular expressions #801 [Sebastian Kanthak] + +* Fixed that SQLite3 exceptions are caught and reported properly #823 [yerejm] + +* Added that all types of after_find/after_initialized callbacks are triggered if the explicit implementation is present, not only the explicit implementation itself + +* Fixed that symbols can be used on attribute assignment, like page.emails.create(:subject => data.subject, :body => data.body) + + +*1.8.0* (7th March, 2005) + +* Added ActiveRecord::Base.colorize_logging to control whether to use colors in logs or not (on by default) + +* Added support for timestamp with time zone in PostgreSQL #560 [Scott Barron] + +* Added MultiparameterAssignmentErrors and AttributeAssignmentError exceptions #777 [demetrius]. Documentation: + + * +MultiparameterAssignmentErrors+ -- collection of errors that occurred during a mass assignment using the + +attributes=+ method. The +errors+ property of this exception contains an array of +AttributeAssignmentError+ + objects that should be inspected to determine which attributes triggered the errors. + * +AttributeAssignmentError+ -- an error occurred while doing a mass assignment through the +attributes=+ method. + You can inspect the +attribute+ property of the exception object to determine which attribute triggered the error. + +* Fixed that postgresql adapter would fails when reading bytea fields with null value #771 [rodrigo k] + +* Added transactional fixtures that uses rollback to undo changes to fixtures instead of DELETE/INSERT -- it's much faster. See documentation under Fixtures #760 [Jeremy Kemper] + +* Added destruction of dependent objects in has_one associations when a new assignment happens #742 [mindel]. Example: + + class Account < ActiveRecord::Base + has_one :credit_card, :dependent => true + end + class CreditCard < ActiveRecord::Base + belongs_to :account + end + + account.credit_card # => returns existing credit card, lets say id = 12 + account.credit_card = CreditCard.create("number" => "123") + account.save # => CC with id = 12 is destroyed + + +* Added validates_numericality_of #716 [skanthak/c.r.mcgrath]. Docuemntation: + + Validates whether the value of the specified attribute is numeric by trying to convert it to + a float with Kernel.Float (if integer is false) or applying it to the regular expression + /^[\+\-]?\d+$/ (if integer is set to true). + + class Person < ActiveRecord::Base + validates_numericality_of :value, :on => :create + end + + Configuration options: + * message - A custom error message (default is: "is not a number") + * on Specifies when this validation is active (default is :save, other options :create, :update) + * only_integer Specifies whether the value has to be an integer, e.g. an integral value (default is false) + + +* Fixed that HasManyAssociation#count was using :finder_sql rather than :counter_sql if it was available #445 [Scott Barron] + +* Added better defaults for composed_of, so statements like composed_of :time_zone, :mapping => %w( time_zone time_zone ) can be written without the mapping part (it's now assumed) + +* Added MacroReflection#macro which will return a symbol describing the macro used (like :composed_of or :has_many) #718, #248 [james@slashetc.com] + + +*1.7.0* (24th February, 2005) + +* Changed the auto-timestamping feature to use ActiveRecord::Base.default_timezone instead of entertaining the parallel ActiveRecord::Base.timestamps_gmt method. The latter is now deprecated and will throw a warning on use (but still work) #710 [Jamis Buck] + +* Added a OCI8-based Oracle adapter that has been verified to work with Oracle 8 and 9 #629 [Graham Jenkins]. Usage notes: + + 1. Key generation uses a sequence "rails_sequence" for all tables. (I couldn't find a simple + and safe way of passing table-specific sequence information to the adapter.) + 2. Oracle uses DATE or TIMESTAMP datatypes for both dates and times. Consequently I have had to + resort to some hacks to get data converted to Date or Time in Ruby. + If the column_name ends in _at (like created_at, updated_at) it's created as a Ruby Time. Else if the + hours/minutes/seconds are 0, I make it a Ruby Date. Else it's a Ruby Time. + This is nasty - but if you use Duck Typing you'll probably not care very much. + In 9i it's tempting to map DATE to Date and TIMESTAMP to Time but I don't think that is + valid - too many databases use DATE for both. + Timezones and sub-second precision on timestamps are not supported. + 3. Default values that are functions (such as "SYSDATE") are not supported. This is a + restriction of the way active record supports default values. + 4. Referential integrity constraints are not fully supported. Under at least + some circumstances, active record appears to delete parent and child records out of + sequence and out of transaction scope. (Or this may just be a problem of test setup.) + + The OCI8 driver can be retrieved from http://rubyforge.org/projects/ruby-oci8/ + +* Added option :schema_order to the PostgreSQL adapter to support the use of multiple schemas per database #697 [YuriSchimke] + +* Optimized the SQL used to generate has_and_belongs_to_many queries by listing the join table first #693 [yerejm] + +* Fixed that when using validation macros with a custom message, if you happened to use single quotes in the message string you would get a parsing error #657 [tonka] + +* Fixed that Active Record would throw Broken Pipe errors with FCGI when the MySQL connection timed out instead of reconnecting #428 [Nicholas Seckar] + +* Added options to specify an SSL connection for MySQL. Define the following attributes in the connection config (config/database.yml in Rails) to use it: sslkey, sslcert, sslca, sslcapath, sslcipher. To use SSL with no client certs, just set :sslca = '/dev/null'. http://dev.mysql.com/doc/mysql/en/secure-connections.html #604 [daniel@nightrunner.com] + +* Added automatic dropping/creating of test tables for running the unit tests on all databases #587 [adelle@bullet.net.au] + +* Fixed that find_by_* would fail when column names had numbers #670 [demetrius] + +* Fixed the SQL Server adapter on a bunch of issues #667 [DeLynn] + + 1. Created a new columns method that is much cleaner. + 2. Corrected a problem with the select and select_all methods + that didn't account for the LIMIT clause being passed into raw SQL statements. + 3. Implemented the string_to_time method in order to create proper instances of the time class. + 4. Added logic to the simplified_type method that allows the database to specify the scale of float data. + 5. Adjusted the quote_column_name to account for the fact that MS SQL is bothered by a forward slash in the data string. + +* Fixed that the dynamic finder like find_all_by_something_boolean(false) didn't work #649 [lmarlow@yahoo.com] + +* Added validates_each that validates each specified attribute against a block #610 [Jeremy Kemper]. Example: + + class Person < ActiveRecord::Base + validates_each :first_name, :last_name do |record, attr| + record.errors.add attr, 'starts with z.' if attr[0] == ?z + end + end + +* Added :allow_nil as an explicit option for validates_length_of, so unless that's set to true having the attribute as nil will also return an error if a range is specified as :within #610 [Jeremy Kemper] + +* Added that validates_* now accept blocks to perform validations #618 [Tim Bates]. Example: + + class Person < ActiveRecord::Base + validate { |person| person.errors.add("title", "will never be valid") if SHOULD_NEVER_BE_VALID } + end + +* Addded validation for validate all the associated objects before declaring failure with validates_associated #618 [Tim Bates] + +* Added keyword-style approach to defining the custom relational bindings #545 [Jamis Buck]. Example: + + class Project < ActiveRecord::Base + primary_key "sysid" + table_name "XYZ_PROJECT" + inheritance_column { original_inheritance_column + "_id" } + end + +* Fixed Base#clone for use with PostgreSQL #565 [hanson@surgery.wisc.edu] + + +*1.6.0* (January 25th, 2005) + +* Added that has_many association build and create methods can take arrays of record data like Base#create and Base#build to build/create multiple records at once. + +* Added that Base#delete and Base#destroy both can take an array of ids to delete/destroy #336 + +* Added the option of supplying an array of attributes to Base#create, so that multiple records can be created at once. + +* Added the option of supplying an array of ids and attributes to Base#update, so that multiple records can be updated at once (inspired by #526/Duane Johnson). Example + + people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} } + Person.update(people.keys, people.values) + +* Added ActiveRecord::Base.timestamps_gmt that can be set to true to make the automated timestamping use GMT instead of local time #520 [Scott Baron] + +* Added that update_all calls sanitize_sql on its updates argument, so stuff like MyRecord.update_all(['time = ?', Time.now]) works #519 [notahat] + +* Fixed that the dynamic finders didn't treat nil as a "IS NULL" but rather "= NULL" case #515 [Demetrius] + +* Added bind-named arrays for interpolating a group of ids or strings in conditions #528 [Jeremy Kemper] + +* Added that has_and_belongs_to_many associations with additional attributes also can be created between unsaved objects and only committed to the database when Base#save is called on the associator #524 [Eric Anderson] + +* Fixed that records fetched with piggy-back attributes or through rich has_and_belongs_to_many associations couldn't be saved due to the extra attributes not part of the table #522 [Eric Anderson] + +* Added mass-assignment protection for the inheritance column -- regardless of a custom column is used or not + +* Fixed that association proxies would fail === tests like PremiumSubscription === @account.subscription + +* Fixed that column aliases didn't work as expected with the new MySql411 driver #507 [Demetrius] + +* Fixed that find_all would produce invalid sql when called sequentialy #490 [Scott Baron] + + +*1.5.1* (January 18th, 2005) + +* Fixed that the belongs_to and has_one proxy would fail a test like 'if project.manager' -- this unfortunately also means that you can't call methods like project.manager.build unless there already is a manager on the project #492 [Tim Bates] + +* Fixed that the Ruby/MySQL adapter wouldn't connect if the password was empty #503 [Pelle] + + +*1.5.0* (January 17th, 2005) + +* Fixed that unit tests for MySQL are now run as the "rails" user instead of root #455 [Eric Hodel] + +* Added validates_associated that enables validation of objects in an unsaved association #398 [Tim Bates]. Example: + + class Book < ActiveRecord::Base + has_many :pages + belongs_to :library + + validates_associated :pages, :library + end + +* Added support for associating unsaved objects #402 [Tim Bates]. Rules that govern this addition: + + == Unsaved objects and associations + + You can manipulate objects and associations before they are saved to the database, but there is some special behaviour you should be + aware of, mostly involving the saving of associated objects. + + === One-to-one associations + + * Assigning an object to a has_one association automatically saves that object, and the object being replaced (if there is one), in + order to update their primary keys - except if the parent object is unsaved (new_record? == true). + * If either of these saves fail (due to one of the objects being invalid) the assignment statement returns false and the assignment + is cancelled. + * If you wish to assign an object to a has_one association without saving it, use the #association.build method (documented below). + * Assigning an object to a belongs_to association does not save the object, since the foreign key field belongs on the parent. It does + not save the parent either. + + === Collections + + * Adding an object to a collection (has_many or has_and_belongs_to_many) automatically saves that object, except if the parent object + (the owner of the collection) is not yet stored in the database. + * If saving any of the objects being added to a collection (via #push or similar) fails, then #push returns false. + * You can add an object to a collection without automatically saving it by using the #collection.build method (documented below). + * All unsaved (new_record? == true) members of the collection are automatically saved when the parent is saved. + +* Added replace to associations, so you can do project.manager.replace(new_manager) or project.milestones.replace(new_milestones) #402 [Tim Bates] + +* Added build and create methods to has_one and belongs_to associations, so you can now do project.manager.build(attributes) #402 [Tim Bates] + +* Added that if a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks defined as methods on the model, which are called last. #402 [Tim Bates] + +* Fixed that Base#== wouldn't work for multiple references to the same unsaved object #402 [Tim Bates] + +* Fixed binary support for PostgreSQL #444 [alex@byzantine.no] + +* Added a differenciation between AssociationCollection#size and -length. Now AssociationCollection#size returns the size of the + collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and calling collection.size if it has. If + it's more likely than not that the collection does have a size larger than zero and you need to fetch that collection afterwards, + it'll take one less SELECT query if you use length. + +* Added Base#attributes that returns a hash of all the attributes with their names as keys and clones of their objects as values #433 [atyp.de] + +* Fixed that foreign keys named the same as the association would cause stack overflow #437 [Eric Anderson] + +* Fixed default scope of acts_as_list from "1" to "1 = 1", so it'll work in PostgreSQL (among other places) #427 [Alexey] + +* Added Base#reload that reloads the attributes of an object from the database #422 [Andreas Schwarz] + +* Added SQLite3 compatibility through the sqlite3-ruby adapter by Jamis Buck #381 [Jeremy Kemper] + +* Added support for the new protocol spoken by MySQL 4.1.1+ servers for the Ruby/MySQL adapter that ships with Rails #440 [Matt Mower] + +* Added that Observers can use the observes class method instead of overwriting self.observed_class(). + + Before: + class ListSweeper < ActiveRecord::Base + def self.observed_class() [ List, Item ] + end + + After: + class ListSweeper < ActiveRecord::Base + observes List, Item + end + +* Fixed that conditions in has_many and has_and_belongs_to_many should be interpolated just like the finder_sql is + +* Fixed Base#update_attribute to be indifferent to whether a string or symbol is used to describe the name + +* Added Base#toggle(attribute) and Base#toggle!(attribute) that makes it easier to flip a switch or flag. + + Before: topic.update_attribute(:approved, !approved?) + After : topic.toggle!(:approved) + +* Added Base#increment!(attribute) and Base#decrement!(attribute) that also saves the records. Example: + + page.views # => 1 + page.increment!(:views) # executes an UPDATE statement + page.views # => 2 + + page.increment(:views).increment!(:views) + page.views # => 4 + +* Added Base#increment(attribute) and Base#decrement(attribute) that encapsulates the += 1 and -= 1 patterns. + + +*1.4.0* (January 4th, 2005) + +* Added automated optimistic locking if the field lock_version is present. Each update to the + record increments the lock_version column and the locking facilities ensure that records instantiated twice + will let the last one saved raise a StaleObjectError if the first was also updated. Example: + + p1 = Person.find(1) + p2 = Person.find(1) + + p1.first_name = "Michael" + p1.save + + p2.first_name = "should fail" + p2.save # Raises a ActiveRecord::StaleObjectError + + You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, + or otherwise apply the business logic needed to resolve the conflict. + + #384 [Michael Koziarski] + +* Added dynamic attribute-based finders as a cleaner way of getting objects by simple queries without turning to SQL. + They work by appending the name of an attribute to find_by_, so you get finders like Person.find_by_user_name, + Payment.find_by_transaction_id. So instead of writing Person.find_first(["user_name = ?", user_name]), you just do + Person.find_by_user_name(user_name). + + It's also possible to use multiple attributes in the same find by separating them with "_and_", so you get finders like + Person.find_by_user_name_and_password or even Payment.find_by_purchaser_and_state_and_country. So instead of writing + Person.find_first(["user_name = ? AND password = ?", user_name, password]), you just do + Person.find_by_user_name_and_password(user_name, password). + + While primarily a construct for easier find_firsts, it can also be used as a construct for find_all by using calls like + Payment.find_all_by_amount(50) that is turned into Payment.find_all(["amount = ?", 50]). This is something not as equally useful, + though, as it's not possible to specify the order in which the objects are returned. + +* Added block-style for callbacks #332 [Jeremy Kemper]. + + Before: + before_destroy(Proc.new{ |record| Person.destroy_all "firm_id = #{record.id}" }) + + After: + before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" } + +* Added :counter_cache option to acts_as_tree that works just like the one you can define on belongs_to #371 [Josh] + +* Added Base.default_timezone accessor that determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling dates + and times from the database. This is set to :local by default. + +* Added the possibility for adapters to overwrite add_limit! to implement a different limiting scheme than "LIMIT X" used by MySQL, PostgreSQL, and SQLite. + +* Added the possibility of having objects with acts_as_list created before their scope is available or... + +* Added a db2 adapter that only depends on the Ruby/DB2 bindings (http://raa.ruby-lang.org/project/ruby-db2/) #386 [Maik Schmidt] + +* Added the final touches to the Microsoft SQL Server adapter by Joey Gibson that makes it suitable for actual use #394 [DeLynn Barry] + +* Added that Base#find takes an optional options hash, including :conditions. Base#find_on_conditions deprecated in favor of #find with :conditions #407 [Jeremy Kemper] + +* Added HasManyAssociation#count that works like Base#count #413 [intinig] + +* Fixed handling of binary content in blobs and similar fields for Ruby/MySQL and SQLite #409 [xal] + +* Fixed a bug in the Ruby/MySQL that caused binary content to be escaped badly and come back mangled #405 [Tobias Luetke] + +* Fixed that the const_missing autoload assumes the requested constant is set by require_association and calls const_get to retrieve it. + If require_association did not set the constant then const_get will call const_missing, resulting in an infinite loop #380 [Jeremy Kemper] + +* Fixed broken transactions that were actually only running object-level and not db level transactions [andreas] + +* Fixed that validates_uniqueness_of used 'id' instead of defined primary key #406 + +* Fixed that the overwritten respond_to? method didn't take two parameters like the original #391 + +* Fixed quoting in validates_format_of that would allow some rules to pass regardless of input #390 [Dmitry V. Sabanin] + + +*1.3.0* (December 23, 2004) + +* Added a require_association hook on const_missing that makes it possible to use any model class without requiring it first. This makes STI look like: + + before: + require_association 'person' + class Employee < Person + end + + after: + class Employee < Person + end + + This also reduces the usefulness of Controller.model in Action Pack to currently only being for documentation purposes. + +* Added that Base.update_all and Base.delete_all return an integer of the number of affected rows #341 + +* Added scope option to validation_uniqueness #349 [Kent Sibilev] + +* Added respondence to *_before_type_cast for all attributes to return their string-state before they were type casted by the column type. + This is helpful for getting "100,000" back on a integer-based validation where the value would normally be "100". + +* Added allow_nil options to validates_inclusion_of so that validation is only triggered if the attribute is not nil [what-a-day] + +* Added work-around for PostgreSQL and the problem of getting fixtures to be created from id 1 on each test case. + This only works for auto-incrementing primary keys called "id" for now #359 [Scott Baron] + +* Added Base#clear_association_cache to empty all the cached associations #347 [Tobias Luetke] + +* Added more informative exceptions in establish_connection #356 [Jeremy Kemper] + +* Added Base#update_attributes that'll accept a hash of attributes and save the record (returning true if it passed validation, false otherwise). + + Before: + person.attributes = @params["person"] + person.save + + Now: + person.update_attributes(@params["person"]) + +* Added Base.destroy and Base.delete to remove records without holding a reference to them first. + +* Added that query benchmarking will only happen if its going to be logged anyway #344 + +* Added higher_item and lower_item as public methods for acts_as_list #342 [Tobias Luetke] + +* Fixed that options[:counter_sql] was overwritten with interpolated sql rather than original sql #355 [Jeremy Kemper] + +* Fixed that overriding an attribute's accessor would be disregarded by add_on_empty and add_on_boundary_breaking because they simply used + the attributes[] hash instead of checking for @base.respond_to?(attr.to_s). [Marten] + +* Fixed that Base.table_name would expect a parameter when used in has_and_belongs_to_many joins [Anna Lissa Cruz] + +* Fixed that nested transactions now work by letting the outer most transaction have the responsibilty of starting and rolling back the transaction. + If any of the inner transactions swallow the exception raised, though, the transaction will not be rolled back. So always let the transaction + bubble up even when you've dealt with local issues. Closes #231 and #340. + +* Fixed validates_{confirmation,acceptance}_of to only happen when the virtual attributes are not nil #348 [dpiddy@gmail.com] + +* Changed the interface on AbstractAdapter to require that adapters return the number of affected rows on delete and update operations. + +* Fixed the automated timestamping feature when running under Rails' development environment that resets the inheritable attributes on each request. + + + +*1.2.0* + +* Added Base.validates_inclusion_of that validates whether the value of the specified attribute is available in a particular enumerable + object. [what-a-day] + + class Person < ActiveRecord::Base + validates_inclusion_of :gender, :in=>%w( m f ), :message=>"woah! what are you then!??!!" + validates_inclusion_of :age, :in=>0..99 + end + +* Added acts_as_list that can decorates an existing class with methods like move_higher/lower, move_to_top/bottom. [Tobias Luetke] Example: + + class TodoItem < ActiveRecord::Base + acts_as_list :scope => :todo_list_id + belongs_to :todo_list + end + +* Added acts_as_tree that can decorates an existing class with a many to many relationship with itself. Perfect for categories in + categories and the likes. [Tobias Luetke] + +* Added that Active Records will automatically record creation and/or update timestamps of database objects if fields of the names + created_at/created_on or updated_at/updated_on are present. [Tobias Luetke] + +* Added Base.default_error_messages as a hash of all the error messages used in the validates_*_of so they can be changed in one place [Tobias Luetke] + +* Added automatic transaction block around AssociationCollection.<<, AssociationCollection.delete, and AssociationCollection.destroy_all + +* Fixed that Base#find will return an array if given an array -- regardless of the number of elements #270 [Marten] + +* Fixed that has_and_belongs_to_many would generate bad sql when naming conventions differed from using vanilla "id" everywhere [RedTerror] + +* Added a better exception for when a type column is used in a table without the intention of triggering single-table inheritance. Example: + + ActiveRecord::SubclassNotFound: The single-table inheritance mechanism failed to locate the subclass: 'bad_class!'. + This error is raised because the column 'type' is reserved for storing the class in case of inheritance. + Please rename this column if you didn't intend it to be used for storing the inheritance class or + overwrite Company.inheritance_column to use another column for that information. + +* Added that single-table inheritance will only kick in if the inheritance_column (by default "type") is present. Otherwise, inheritance won't + have any magic side effects. + +* Added the possibility of marking fields as being in error without adding a message (using nil) to it that'll get displayed wth full_messages #208 [mjobin] + +* Fixed Base.errors to be indifferent as to whether strings or symbols are used. Examples: + + Before: + errors.add(:name, "must be shorter") if name.size > 10 + errors.on(:name) # => "must be shorter" + errors.on("name") # => nil + + After: + errors.add(:name, "must be shorter") if name.size > 10 + errors.on(:name) # => "must be shorter" + errors.on("name") # => "must be shorter" + +* Added Base.validates_format_of that Validates whether the value of the specified attribute is of the correct form by matching + it against the regular expression provided. [Marcel] + + class Person < ActiveRecord::Base + validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/, :on => :create + end + +* Added Base.validates_length_of that delegates to add_on_boundary_breaking #312 [Tobias Luetke]. Example: + + Validates that the specified attribute matches the length restrictions supplied in either: + + - configuration[:minimum] + - configuration[:maximum] + - configuration[:is] + - configuration[:within] (aka. configuration[:in]) + + Only one option can be used at a time. + + class Person < ActiveRecord::Base + validates_length_of :first_name, :maximum=>30 + validates_length_of :last_name, :maximum=>30, :message=>"less than %d if you don't mind" + validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name" + validates_length_of :fav_bra_size, :minimum=>1, :too_short=>"please enter at least %d character" + validates_length_of :smurf_leader, :is=>4, :message=>"papa is spelled with %d characters... don't play me." + end + +* Added Base.validate_presence as an alternative to implementing validate and doing errors.add_on_empty yourself. + +* Added Base.validates_uniqueness_of that alidates whether the value of the specified attributes are unique across the system. + Useful for making sure that only one user can be named "davidhh". + + class Person < ActiveRecord::Base + validates_uniqueness_of :user_name + end + + When the record is created, a check is performed to make sure that no record exist in the database with the given value for the specified + attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself. + + +* Added Base.validates_confirmation_of that encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example: + + Model: + class Person < ActiveRecord::Base + validates_confirmation_of :password + end + + View: + <%= password_field "person", "password" %> + <%= password_field "person", "password_confirmation" %> + + The person has to already have a password attribute (a column in the people table), but the password_confirmation is virtual. + It exists only as an in-memory variable for validating the password. This check is performed both on create and update. + + +* Added Base.validates_acceptance_of that encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example: + + class Person < ActiveRecord::Base + validates_acceptance_of :terms_of_service + end + + The terms_of_service attribute is entirely virtual. No database column is needed. This check is performed both on create and update. + + NOTE: The agreement is considered valid if it's set to the string "1". This makes it easy to relate it to an HTML checkbox. + + +* Added validation macros to make the stackable just like the lifecycle callbacks. Examples: + + class Person < ActiveRecord::Base + validate { |record| record.errors.add("name", "too short") unless name.size > 10 } + validate { |record| record.errors.add("name", "too long") unless name.size < 20 } + validate_on_create :validate_password + + private + def validate_password + errors.add("password", "too short") unless password.size > 6 + end + end + +* Added the option for sanitizing find_by_sql and the offset parts in regular finds [Sam Stephenson]. Examples: + + Project.find_all ["category = ?", category_name], "created ASC", ["? OFFSET ?", 15, 20] + Post.find_by_sql ["SELECT * FROM posts WHERE author = ? AND created > ?", author_id, start_date] + +* Fixed value quoting in all generated SQL statements, so that integers are not surrounded in quotes and that all sanitation are happening + through the database's own quoting routine. This should hopefully make it lots easier for new adapters that doesn't accept '1' for integer + columns. + +* Fixed has_and_belongs_to_many guessing of foreign key so that keys are generated correctly for models like SomeVerySpecialClient + [Florian Weber] + +* Added counter_sql option for has_many associations [Jeremy Kemper]. Documentation: + + :counter_sql - specify a complete SQL statement to fetch the size of the association. If +:finder_sql+ is + specified but +:counter_sql+, +:counter_sql+ will be generated by replacing SELECT ... FROM with SELECT COUNT(*) FROM. + +* Fixed that methods wrapped in callbacks still return their original result #260 [Jeremy Kemper] + +* Fixed the Inflector to handle the movie/movies pair correctly #261 [Scott Baron] + +* Added named bind-style variable interpolation #281 [Michael Koziarski]. Example: + + Person.find(["id = :id and first_name = :first_name", { :id => 5, :first_name = "bob' or 1=1" }]) + +* Added bind-style variable interpolation for the condition arrays that uses the adapter's quote method [Michael Koziarski] + + Before: + find_first([ "user_name = '%s' AND password = '%s'", user_name, password ])] + find_first([ "firm_id = %s", firm_id ])] # unsafe! + + After: + find_first([ "user_name = ? AND password = ?", user_name, password ])] + find_first([ "firm_id = ?", firm_id ])] + +* Added CSV format for fixtures #272 [what-a-day]. (See the new and expanded documentation on fixtures for more information) + +* Fixed fixtures using primary key fields called something else than "id" [dave] + +* Added proper handling of time fields that are turned into Time objects with the dummy date of 2000/1/1 [HariSeldon] + +* Added reverse order of deleting fixtures, so referential keys can be maintained #247 [Tim Bates] + +* Added relative path search for sqlite dbfiles in database.yml (if RAILS_ROOT is defined) #233 [Jeremy Kemper] + +* Added option to establish_connection where you'll be able to leave out the parameter to have it use the RAILS_ENV environment variable + +* Fixed problems with primary keys and postgresql sequences (#230) [Tim Bates] + +* Added reloading for associations under cached environments like FastCGI and mod_ruby. This makes it possible to use those environments for development. + This is turned on by default, but can be turned off with ActiveRecord::Base.reload_dependencies = false in production environments. + + NOTE: This will only have an effect if you let the associations manage the requiring of model classes. All libraries loaded through + require will be "forever" cached. You can, however, use ActiveRecord::Base.load_or_require("library") to get this behavior outside of the + auto-loading associations. + +* Added ERB capabilities to the fixture files for dynamic fixture generation. You don't need to do anything, just include ERB blocks like: + + david: + id: 1 + name: David + + jamis: + id: 2 + name: Jamis + + <% for digit in 3..10 %> + dev_<%= digit %>: + id: <%= digit %> + name: fixture_<%= digit %> + <% end %> + +* Changed the yaml fixture searcher to look in the root of the fixtures directory, so when you before could have something like: + + fixtures/developers/fixtures.yaml + fixtures/accounts/fixtures.yaml + + ...you now need to do: + + fixtures/developers.yaml + fixtures/accounts.yaml + +* Changed the fixture format from: + + name: david + data: + id: 1 + name: David Heinemeier Hansson + birthday: 1979-10-15 + profession: Systems development + --- + name: steve + data: + id: 2 + name: Steve Ross Kellock + birthday: 1974-09-27 + profession: guy with keyboard + + ...to: + + david: + id: 1 + name: David Heinemeier Hansson + birthday: 1979-10-15 + profession: Systems development + + steve: + id: 2 + name: Steve Ross Kellock + birthday: 1974-09-27 + profession: guy with keyboard + + The change is NOT backwards compatible. Fixtures written in the old YAML style needs to be rewritten! + +* All associations will now attempt to require the classes that they associate to. Relieving the need for most explicit 'require' statements. + + +*1.1.0* (34) + +* Added automatic fixture setup and instance variable availability. Fixtures can also be automatically + instantiated in instance variables relating to their names using the following style: + + class FixturesTest < Test::Unit::TestCase + fixtures :developers # you can add more with comma separation + + def test_developers + assert_equal 3, @developers.size # the container for all the fixtures is automatically set + assert_kind_of Developer, @david # works like @developers["david"].find + assert_equal "David Heinemeier Hansson", @david.name + end + end + +* Added HasAndBelongsToManyAssociation#push_with_attributes(object, join_attributes) that can create associations in the join table with additional + attributes. This is really useful when you have information that's only relevant to the join itself, such as a "added_on" column for an association + between post and category. The added attributes will automatically be injected into objects retrieved through the association similar to the piggy-back + approach: + + post.categories.push_with_attributes(category, :added_on => Date.today) + post.categories.first.added_on # => Date.today + + NOTE: The categories table doesn't have a added_on column, it's the categories_post join table that does! + +* Fixed that :exclusively_dependent and :dependent can't be activated at the same time on has_many associations [Jeremy Kemper] + +* Fixed that database passwords couldn't be all numeric [Jeremy Kemper] + +* Fixed that calling id would create the instance variable for new_records preventing them from being saved correctly [Jeremy Kemper] + +* Added sanitization feature to HasManyAssociation#find_all so it works just like Base.find_all [Sam Stephenson/bitsweat] + +* Added that you can pass overlapping ids to find without getting duplicated records back [Jeremy Kemper] + +* Added that Base.benchmark returns the result of the block [Jeremy Kemper] + +* Fixed problem with unit tests on Windows with SQLite [paterno] + +* Fixed that quotes would break regular non-yaml fixtures [Dmitry Sabanin/daft] + +* Fixed fixtures on windows with line endings cause problems under unix / mac [Tobias Luetke] + +* Added HasAndBelongsToManyAssociation#find(id) that'll search inside the collection and find the object or record with that id + +* Added :conditions option to has_and_belongs_to_many that works just like the one on all the other associations + +* Added AssociationCollection#clear to remove all associations from has_many and has_and_belongs_to_many associations without destroying the records [geech] + +* Added type-checking and remove in 1-instead-of-N sql statements to AssociationCollection#delete [geech] + +* Added a return of self to AssociationCollection#<< so appending can be chained, like project << Milestone.create << Milestone.create [geech] + +* Added Base#hash and Base#eql? which means that all of the equality using features of array and other containers now works: + + [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] + +* Added :uniq as an option to has_and_belongs_to_many which will automatically ensure that AssociateCollection#uniq is called + before pulling records out of the association. This is especially useful for three-way (and above) has_and_belongs_to_many associations. + +* Added AssociateCollection#uniq which is especially useful for has_and_belongs_to_many associations that can include duplicates, + which is common on associations that also use metadata. Usage: post.categories.uniq + +* Fixed respond_to? to use a subclass specific hash instead of an Active Record-wide one + +* Fixed has_and_belongs_to_many to treat associations between classes in modules properly [Florian Weber] + +* Added a NoMethod exception to be raised when query and writer methods are called for attributes that doesn't exist [geech] + +* Added a more robust version of Fixtures that throws meaningful errors when on formatting issues [geech] + +* Added Base#transaction as a compliment to Base.transaction for prettier use in instance methods [geech] + +* Improved the speed of respond_to? by placing the dynamic methods lookup table in a hash [geech] + +* Added that any additional fields added to the join table in a has_and_belongs_to_many association + will be placed as attributes when pulling records out through has_and_belongs_to_many associations. + This is helpful when have information about the association itself that you want available on retrival. + +* Added better loading exception catching and RubyGems retries to the database adapters [alexeyv] + +* Fixed bug with per-model transactions [daniel] + +* Fixed Base#transaction so that it returns the result of the last expression in the transaction block [alexeyv] + +* Added Fixture#find to find the record corresponding to the fixture id. The record + class name is guessed by using Inflector#classify (also new) on the fixture directory name. + + Before: Document.find(@documents["first"]["id"]) + After : @documents["first"].find + +* Fixed that the table name part of column names ("TABLE.COLUMN") wasn't removed properly [Andreas Schwarz] + +* Fixed a bug with Base#size when a finder_sql was used that didn't capitalize SELECT and FROM [geech] + +* Fixed quoting problems on SQLite by adding quote_string to the AbstractAdapter that can be overwritten by the concrete + adapters for a call to the dbm. [Andreas Schwarz] + +* Removed RubyGems backup strategy for requiring SQLite-adapter -- if people want to use gems, they're already doing it with AR. + + +*1.0.0 (35)* + +* Added OO-style associations methods [Florian Weber]. Examples: + + Project#milestones_count => Project#milestones.size + Project#build_to_milestones => Project#milestones.build + Project#create_for_milestones => Project#milestones.create + Project#find_in_milestones => Project#milestones.find + Project#find_all_in_milestones => Project#milestones.find_all + +* Added serialize as a new class method to control when text attributes should be YAMLized or not. This means that automated + serialization of hashes, arrays, and so on WILL NO LONGER HAPPEN (#10). You need to do something like this: + + class User < ActiveRecord::Base + serialize :settings + end + + This will assume that settings is a text column and will now YAMLize any object put in that attribute. You can also specify + an optional :class_name option that'll raise an exception if a serialized object is retrieved as a descendent of a class not in + the hierarchy. Example: + + class User < ActiveRecord::Base + serialize :settings, :class_name => "Hash" + end + + user = User.create("settings" => %w( one two three )) + User.find(user.id).settings # => raises SerializationTypeMismatch + +* Added the option to connect to a different database for one model at a time. Just call establish_connection on the class + you want to have connected to another database than Base. This will automatically also connect decendents of that class + to the different database [Renald Buter]. + +* Added transactional protection for Base#save. Validations can now check for values knowing that it happens in a transaction and callbacks + can raise exceptions knowing that the save will be rolled back. [Suggested by Alexey Verkhovsky] + +* Added column name quoting so reserved words, such as "references", can be used as column names [Ryan Platte] + +* Added the possibility to chain the return of what happened inside a logged block [geech]: + + This now works: + log { ... }.map { ... } + + Instead of doing: + result = [] + log { result = ... } + result.map { ... } + +* Added "socket" option for the MySQL adapter, so you can change it to something else than "/tmp/mysql.sock" [Anna Lissa Cruz] + +* Added respond_to? answers for all the attribute methods. So if Person has a name attribute retrieved from the table schema, + person.respond_to? "name" will return true. + +* Added Base.benchmark which can be used to aggregate logging and benchmark, so you can measure and represent multiple statements in a single block. + Usage (hides all the SQL calls for the individual actions and calculates total runtime for them all): + + Project.benchmark("Creating project") do + project = Project.create("name" => "stuff") + project.create_manager("name" => "David") + project.milestones << Milestone.find_all + end + +* Added logging of invalid SQL statements [Suggested by Daniel Von Fange] + +* Added alias Errors#[] for Errors#on, so you can now say person.errors["name"] to retrieve the errors for name [Andreas Schwarz] + +* Added RubyGems require attempt if sqlite-ruby is not available through regular methods. + +* Added compatibility with 2.x series of sqlite-ruby drivers. [Jamis Buck] + +* Added type safety for association assignments, so a ActiveRecord::AssociationTypeMismatch will be raised if you attempt to + assign an object that's not of the associated class. This cures the problem with nil giving id = 4 and fixnums giving id = 1 on + mistaken association assignments. [Reported by Andreas Schwarz] + +* Added the option to keep many fixtures in one single YAML document [what-a-day] + +* Added the class method "inheritance_column" that can be overwritten to return the name of an alternative column than "type" for storing + the type for inheritance hierarchies. [Dave Steinberg] + +* Added [] and []= as an alternative way to access attributes when the regular methods have been overwritten [Dave Steinberg] + +* Added the option to observer more than one class at the time by specifying observed_class as an array + +* Added auto-id propagation support for tables with arbitrary primary keys that have autogenerated sequences associated with them + on PostgreSQL. [Dave Steinberg] + +* Changed that integer and floats set to "" through attributes= remain as NULL. This was especially a problem for scaffolding and postgresql. (#49) + +* Changed the MySQL Adapter to rely on MySQL for its defaults for socket, host, and port [Andreas Schwarz] + +* Changed ActionControllerError to decent from StandardError instead of Exception. It can now be caught by a generic rescue. + +* Changed class inheritable attributes to not use eval [Caio Chassot] + +* Changed Errors#add to now use "invalid" as the default message instead of true, which means full_messages work with those [Marcel Molina Jr] + +* Fixed spelling on Base#add_on_boundry_breaking to Base#add_on_boundary_breaking (old naming still works) [Marcel Molina Jr.] + +* Fixed that entries in the has_and_belongs_to_many join table didn't get removed when an associated object was destroyed. + +* Fixed unnecessary calls to SET AUTOCOMMIT=0/1 for MySQL adapter [Andreas Schwarz] + +* Fixed PostgreSQL defaults are now handled gracefully [Dave Steinberg] + +* Fixed increment/decrement_counter are now atomic updates [Andreas Schwarz] + +* Fixed the problems the Inflector had turning Attachment into attuchments and Cases into Casis [radsaq/Florian Gross] + +* Fixed that cloned records would point attribute references on the parent object [Andreas Schwarz] + +* Fixed SQL for type call on inheritance hierarchies [Caio Chassot] + +* Fixed bug with typed inheritance [Florian Weber] + +* Fixed a bug where has_many collection_count wouldn't use the conditions specified for that association + + +*0.9.5* + +* Expanded the table_name guessing rules immensely [Florian Green]. Documentation: + + Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending + directly from ActiveRecord. So if the hierarchy looks like: Reply < Message < ActiveRecord, then Message is used + to guess the table name from even when called on Reply. The guessing rules are as follows: + * Class name ends in "x", "ch" or "ss": "es" is appended, so a Search class becomes a searches table. + * Class name ends in "y" preceded by a consonant or "qu": The "y" is replaced with "ies", + so a Category class becomes a categories table. + * Class name ends in "fe": The "fe" is replaced with "ves", so a Wife class becomes a wives table. + * Class name ends in "lf" or "rf": The "f" is replaced with "ves", so a Half class becomes a halves table. + * Class name ends in "person": The "person" is replaced with "people", so a Salesperson class becomes a salespeople table. + * Class name ends in "man": The "man" is replaced with "men", so a Spokesman class becomes a spokesmen table. + * Class name ends in "sis": The "i" is replaced with an "e", so a Basis class becomes a bases table. + * Class name ends in "tum" or "ium": The "um" is replaced with an "a", so a Datum class becomes a data table. + * Class name ends in "child": The "child" is replaced with "children", so a NodeChild class becomes a node_children table. + * Class name ends in an "s": No additional characters are added or removed. + * Class name doesn't end in "s": An "s" is appended, so a Comment class becomes a comments table. + * Class name with word compositions: Compositions are underscored, so CreditCard class becomes a credit_cards table. + Additionally, the class-level table_name_prefix is prepended to the table_name and the table_name_suffix is appended. + So if you have "myapp_" as a prefix, the table name guess for an Account class becomes "myapp_accounts". + + You can also overwrite this class method to allow for unguessable links, such as a Mouse class with a link to a + "mice" table. Example: + + class Mouse < ActiveRecord::Base + def self.table_name() "mice" end + end + + This conversion is now done through an external class called Inflector residing in lib/active_record/support/inflector.rb. + +* Added find_all_in_collection to has_many defined collections. Works like this: + + class Firm < ActiveRecord::Base + has_many :clients + end + + firm.id # => 1 + firm.find_all_in_clients "revenue > 1000" # SELECT * FROM clients WHERE firm_id = 1 AND revenue > 1000 + + [Requested by Dave Thomas] + +* Fixed finders for inheritance hierarchies deeper than one level [Florian Weber] + +* Added add_on_boundry_breaking to errors to accompany add_on_empty as a default validation method. It's used like this: + + class Person < ActiveRecord::Base + protected + def validation + errors.add_on_boundry_breaking "password", 3..20 + end + end + + This will add an error to the tune of "is too short (minimum is 3 characters)" or "is too long (minimum is 20 characters)" if + the password is outside the boundry. The messages can be changed by passing a third and forth parameter as message strings. + +* Implemented a clone method that works properly with AR. It returns a clone of the record that + hasn't been assigned an id yet and is treated as a new record. + +* Allow for domain sockets in PostgreSQL by not assuming localhost when no host is specified [Scott Barron] + +* Fixed that bignums are saved properly instead of attempted to be YAMLized [Andreas Schwartz] + +* Fixed a bug in the GEM where the rdoc options weren't being passed according to spec [Chad Fowler] + +* Fixed a bug with the exclusively_dependent option for has_many + + +*0.9.4* + +* Correctly guesses the primary key when the class is inside a module [Dave Steinberg]. + +* Added [] and []= as alternatives to read_attribute and write_attribute [Dave Steinberg] + +* has_and_belongs_to_many now accepts an :order key to determine in which order the collection is returned [radsaq]. + +* The ids passed to find and find_on_conditions are now automatically sanitized. + +* Added escaping of plings in YAML content. + +* Multi-parameter assigns where all the parameters are empty will now be set to nil instead of a new instance of their class. + +* Proper type within an inheritance hierarchy is now ensured already at object initialization (instead of first at create) + + +*0.9.3* + +* Fixed bug with using a different primary key name together with has_and_belongs_to_many [Investigation by Scott] + +* Added :exclusively_dependent option to the has_many association macro. The doc reads: + + If set to true all the associated object are deleted in one SQL statement without having their + before_destroy callback run. This should only be used on associations that depend solely on + this class and don't need to do any clean-up in before_destroy. The upside is that it's much + faster, especially if there's a counter_cache involved. + +* Added :port key to connection options, so the PostgreSQL and MySQL adapters can connect to a database server + running on another port than the default. + +* Converted the new natural singleton methods that prevented AR objects from being saved by PStore + (and hence be placed in a Rails session) to a module. [Florian Weber] + +* Fixed the use of floats (was broken since 0.9.0+) + +* Fixed PostgreSQL adapter so default values are displayed properly when used in conjunction with + Action Pack scaffolding. + +* Fixed booleans support for PostgreSQL (use real true/false on boolean fields instead of 0/1 on tinyints) [radsaq] + + +*0.9.2* + +* Added static method for instantly updating a record + +* Treat decimal and numeric as Ruby floats [Andreas Schwartz] + +* Treat chars as Ruby strings (fixes problem for Action Pack form helpers too) + +* Removed debugging output accidently left in (which would screw web applications) + + +*0.9.1* + +* Added MIT license + +* Added natural object-style assignment for has_and_belongs_to_many associations. Consider the following model: + + class Event < ActiveRecord::Base + has_one_and_belongs_to_many :sponsors + end + + class Sponsor < ActiveRecord::Base + has_one_and_belongs_to_many :sponsors + end + + Earlier, you'd have to use synthetic methods for creating associations between two objects of the above class: + + roskilde_festival.add_to_sponsors(carlsberg) + roskilde_festival.remove_from_sponsors(carlsberg) + + nike.add_to_events(world_cup) + nike.remove_from_events(world_cup) + + Now you can use regular array-styled methods: + + roskilde_festival.sponsors << carlsberg + roskilde_festival.sponsors.delete(carlsberg) + + nike.events << world_cup + nike.events.delete(world_cup) + +* Added delete method for has_many associations. Using this will nullify an association between the has_many and the belonging + object by setting the foreign key to null. Consider this model: + + class Post < ActiveRecord::Base + has_many :comments + end + + class Comment < ActiveRecord::Base + belongs_to :post + end + + You could do something like: + + funny_comment.has_post? # => true + announcement.comments.delete(funny_comment) + funny_comment.has_post? # => false + + +*0.9.0* + +* Active Record is now thread safe! (So you can use it with Cerise and WEBrick applications) + [Implementation idea by Michael Neumann, debugging assistance by Jamis Buck] + +* Improved performance by roughly 400% on a basic test case of pulling 100 records and querying one attribute. + This brings the tax for using Active Record instead of "riding on the metal" (using MySQL-ruby C-driver directly) down to ~50%. + Done by doing lazy type conversions and caching column information on the class-level. + +* Added callback objects and procs as options for implementing the target for callback macros. + +* Added "counter_cache" option to belongs_to that automates the usage of increment_counter and decrement_counter. Consider: + + class Post < ActiveRecord::Base + has_many :comments + end + + class Comment < ActiveRecord::Base + belongs_to :post + end + + Iterating over 100 posts like this: + + <% for post in @posts %> + <%= post.title %> has <%= post.comments_count %> comments + <% end %> + + Will generate 100 SQL count queries -- one for each call to post.comments_count. If you instead add a "comments_count" int column + to the posts table and rewrite the comments association macro with: + + class Comment < ActiveRecord::Base + belongs_to :post, :counter_cache => true + end + + Those 100 SQL count queries will be reduced to zero. Beware that counter caching is only appropriate for objects that begin life + with the object it's specified to belong with and is destroyed like that as well. Typically objects where you would also specify + :dependent => true. If your objects switch from one belonging to another (like a post that can be move from one category to another), + you'll have to manage the counter yourself. + +* Added natural object-style assignment for has_one and belongs_to associations. Consider the following model: + + class Project < ActiveRecord::Base + has_one :manager + end + + class Manager < ActiveRecord::Base + belongs_to :project + end + + Earlier, assignments would work like following regardless of which way the assignment told the best story: + + active_record.manager_id = david.id + + Now you can do it either from the belonging side: + + david.project = active_record + + ...or from the having side: + + active_record.manager = david + + If the assignment happens from the having side, the assigned object is automatically saved. So in the example above, the + project_id attribute on david would be set to the id of active_record, then david would be saved. + +* Added natural object-style assignment for has_many associations [Florian Weber]. Consider the following model: + + class Project < ActiveRecord::Base + has_many :milestones + end + + class Milestone < ActiveRecord::Base + belongs_to :project + end + + Earlier, assignments would work like following regardless of which way the assignment told the best story: + + deadline.project_id = active_record.id + + Now you can do it either from the belonging side: + + deadline.project = active_record + + ...or from the having side: + + active_record.milestones << deadline + + The milestone is automatically saved with the new foreign key. + +* API CHANGE: Attributes for text (or blob or similar) columns will now have unknown classes stored using YAML instead of using + to_s. (Known classes that won't be yamelized are: String, NilClass, TrueClass, FalseClass, Fixnum, Date, and Time). + Likewise, data pulled out of text-based attributes will be attempted converged using Yaml if they have the "--- " header. + This was primarily done to be enable the storage of hashes and arrays without wrapping them in aggregations, so now you can do: + + user = User.find(1) + user.preferences = { "background" => "black", "display" => large } + user.save + + User.find(1).preferences # => { "background" => "black", "display" => large } + + Please note that this method should only be used when you don't care about representing the object in proper columns in + the database. A money object consisting of an amount and a currency is still a much better fit for a value object done through + aggregations than this new option. + +* POSSIBLE CODE BREAKAGE: As a consequence of the lazy type conversions, it's a bad idea to reference the @attributes hash + directly (it always was, but now it's paramount that you don't). If you do, you won't get the type conversion. So to implement + new accessors for existing attributes, use read_attribute(attr_name) and write_attribute(attr_name, value) instead. Like this: + + class Song < ActiveRecord::Base + # Uses an integer of seconds to hold the length of the song + + def length=(minutes) + write_attribute("length", minutes * 60) + end + + def length + read_attribute("length") / 60 + end + end + + The clever kid will notice that this opens a door to sidestep the automated type conversion by using @attributes directly. + This is not recommended as read/write_attribute may be granted additional responsibilities in the future, but if you think + you know what you're doing and aren't afraid of future consequences, this is an option. + +* Applied a few minor bug fixes reported by Daniel Von Fange. + + +*0.8.4* + +_Reflection_ + +* Added ActiveRecord::Reflection with a bunch of methods and classes for reflecting in aggregations and associations. + +* Added Base.columns and Base.content_columns which returns arrays of column description (type, default, etc) objects. + +* Added Base#attribute_names which returns an array of names for the attributes available on the object. + +* Added Base#column_for_attribute(name) which returns the column description object for the named attribute. + + +_Misc_ + +* Added multi-parameter assignment: + + # Instantiate objects for all attribute classes that needs more than one constructor parameter. This is done + # by calling new on the column type or aggregation type (through composed_of) object with these parameters. + # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate + # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the + # parenteses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, f for Float, + # s for String, and a for Array. + + This is incredibly useful for assigning dates from HTML drop-downs of month, year, and day. + +* Fixed bug with custom primary key column name and Base.find on multiple parameters. + +* Fixed bug with dependent option on has_one associations if there was no associated object. + + +*0.8.3* + +_Transactions_ + +* Added transactional protection for destroy (important for the new :dependent option) [Suggested by Carl Youngblood] + +* Fixed so transactions are ignored on MyISAM tables for MySQL (use InnoDB to get transactions) + +* Changed transactions so only exceptions will cause a rollback, not returned false. + + +_Mapping_ + +* Added support for non-integer primary keys [Aredridel/earlier work by Michael Neumann] + + User.find "jdoe" + Product.find "PDKEY-INT-12" + +* Added option to specify naming method for primary key column. ActiveRecord::Base.primary_key_prefix_type can either + be set to nil, :table_name, or :table_name_with_underscore. :table_name will assume that Product class has a primary key + of "productid" and :table_name_with_underscore will assume "product_id". The default nil will just give "id". + +* Added an overwriteable primary_key method that'll instruct AR to the name of the + id column [Aredridele/earlier work by Guan Yang] + + class Project < ActiveRecord::Base + def self.primary_key() "project_id" end + end + +* Fixed that Active Records can safely associate inside and out of modules. + + class MyApplication::Account < ActiveRecord::Base + has_many :clients # will look for MyApplication::Client + has_many :interests, :class_name => "Business::Interest" # will look for Business::Interest + end + +* Fixed that Active Records can safely live inside modules [Aredridel] + + class MyApplication::Account < ActiveRecord::Base + end + + +_Misc_ + +* Added freeze call to value object assignments to ensure they remain immutable [Spotted by Gavin Sinclair] + +* Changed interface for specifying observed class in observers. Was OBSERVED_CLASS constant, now is + observed_class() class method. This is more consistant with things like self.table_name(). Works like this: + + class AuditObserver < ActiveRecord::Observer + def self.observed_class() Account end + def after_update(account) + AuditTrail.new(account, "UPDATED") + end + end + + [Suggested by Gavin Sinclair] + +* Create new Active Record objects by setting the attributes through a block. Like this: + + person = Person.new do |p| + p.name = 'Freddy' + p.age = 19 + end + + [Suggested by Gavin Sinclair] + + +*0.8.2* + +* Added inheritable callback queues that can ensure that certain callback methods or inline fragments are + run throughout the entire inheritance hierarchy. Regardless of whether a descendent overwrites the callback + method: + + class Topic < ActiveRecord::Base + before_destroy :destroy_author, 'puts "I'm an inline fragment"' + end + + Learn more in link:classes/ActiveRecord/Callbacks.html + +* Added :dependent option to has_many and has_one, which will automatically destroy associated objects when + the holder is destroyed: + + class Album < ActiveRecord::Base + has_many :tracks, :dependent => true + end + + All the associated tracks are destroyed when the album is. + +* Added Base.create as a factory that'll create, save, and return a new object in one step. + +* Automatically convert strings in config hashes to symbols for the _connection methods. This allows you + to pass the argument hashes directly from yaml. (Luke) + +* Fixed the install.rb to include simple.rb [Spotted by Kevin Bullock] + +* Modified block syntax to better follow our code standards outlined in + http://www.rubyonrails.org/CodingStandards + + +*0.8.1* + +* Added object-level transactions [Thanks to Austin Ziegler for Transaction::Simple] + +* Changed adapter-specific connection methods to use centralized ActiveRecord::Base.establish_connection, + which is parametized through a config hash with symbol keys instead of a regular parameter list. + This will allow for database connections to be opened in a more generic fashion. (Luke) + + NOTE: This requires all *_connections to be updated! Read more in: + http://ar.rubyonrails.org/classes/ActiveRecord/Base.html#M000081 + +* Fixed SQLite adapter so objects fetched from has_and_belongs_to_many have proper attributes + (t.name is now name). [Spotted by Garrett Rooney] + +* Fixed SQLite adapter so dates are returned as Date objects, not Time objects [Spotted by Gavin Sinclair] + +* Fixed requirement of date class, so date conversions are succesful regardless of whether you + manually require date or not. + + +*0.8.0* + +* Added transactions + +* Changed Base.find to also accept either a list (1, 5, 6) or an array of ids ([5, 7]) + as parameter and then return an array of objects instead of just an object + +* Fixed method has_collection? for has_and_belongs_to_many macro to behave as a + collection, not an association + +* Fixed SQLite adapter so empty or nil values in columns of datetime, date, or time type + aren't treated as current time [Spotted by Gavin Sinclair] + + +*0.7.6* + +* Fixed the install.rb to create the lib/active_record/support directory [Spotted by Gavin Sinclair] +* Fixed that has_association? would always return true [Spotted by Daniel Von Fange] diff --git a/vendor/rails/activerecord/MIT-LICENSE b/vendor/rails/activerecord/MIT-LICENSE new file mode 100644 index 00000000..5919c288 --- /dev/null +++ b/vendor/rails/activerecord/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2004 David Heinemeier Hansson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/rails/activerecord/README b/vendor/rails/activerecord/README new file mode 100755 index 00000000..1be4df61 --- /dev/null +++ b/vendor/rails/activerecord/README @@ -0,0 +1,360 @@ += Active Record -- Object-relation mapping put on rails + +Active Record connects business objects and database tables to create a persistable +domain model where logic and data are presented in one wrapping. It's an implementation +of the object-relational mapping (ORM) pattern[http://www.martinfowler.com/eaaCatalog/activeRecord.html] +by the same name as described by Martin Fowler: + + "An object that wraps a row in a database table or view, encapsulates + the database access, and adds domain logic on that data." + +Active Record's main contribution to the pattern is to relieve the original of two stunting problems: +lack of associations and inheritance. By adding a simple domain language-like set of macros to describe +the former and integrating the Single Table Inheritance pattern for the latter, Active Record narrows the +gap of functionality between the data mapper and active record approach. + +A short rundown of the major features: + +* Automated mapping between classes and tables, attributes and columns. + + class Product < ActiveRecord::Base; end + + ...is automatically mapped to the table named "products", such as: + + CREATE TABLE products ( + id int(11) NOT NULL auto_increment, + name varchar(255), + PRIMARY KEY (id) + ); + + ...which again gives Product#name and Product#name=(new_name) + + {Learn more}[link:classes/ActiveRecord/Base.html] + + +* Associations between objects controlled by simple meta-programming macros. + + class Firm < ActiveRecord::Base + has_many :clients + has_one :account + belongs_to :conglomorate + end + + {Learn more}[link:classes/ActiveRecord/Associations/ClassMethods.html] + + +* Aggregations of value objects controlled by simple meta-programming macros. + + class Account < ActiveRecord::Base + composed_of :balance, :class_name => "Money", + :mapping => %w(balance amount) + composed_of :address, + :mapping => [%w(address_street street), %w(address_city city)] + end + + {Learn more}[link:classes/ActiveRecord/Aggregations/ClassMethods.html] + + +* Validation rules that can differ for new or existing objects. + + class Account < ActiveRecord::Base + validates_presence_of :subdomain, :name, :email_address, :password + validates_uniqueness_of :subdomain + validates_acceptance_of :terms_of_service, :on => :create + validates_confirmation_of :password, :email_address, :on => :create + end + + {Learn more}[link:classes/ActiveRecord/Validations.html] + + +* Acts that can make records work as lists or trees: + + class Item < ActiveRecord::Base + belongs_to :list + acts_as_list :scope => :list + end + + item.move_higher + item.move_to_bottom + + Learn about {acts_as_list}[link:classes/ActiveRecord/Acts/List/ClassMethods.html], {the instance methods acts_as_list provides}[link:classes/ActiveRecord/Acts/List/InstanceMethods.html], and + {acts_as_tree}[link:classes/ActiveRecord/Acts/Tree/ClassMethods.html] + +* Callbacks as methods or queues on the entire lifecycle (instantiation, saving, destroying, validating, etc). + + class Person < ActiveRecord::Base + def before_destroy # is called just before Person#destroy + CreditCard.find(credit_card_id).destroy + end + end + + class Account < ActiveRecord::Base + after_find :eager_load, 'self.class.announce(#{id})' + end + + {Learn more}[link:classes/ActiveRecord/Callbacks.html] + + +* Observers for the entire lifecycle + + class CommentObserver < ActiveRecord::Observer + def after_create(comment) # is called just after Comment#save + Notifications.deliver_new_comment("david@loudthinking.com", comment) + end + end + + {Learn more}[link:classes/ActiveRecord/Observer.html] + + +* Inheritance hierarchies + + class Company < ActiveRecord::Base; end + class Firm < Company; end + class Client < Company; end + class PriorityClient < Client; end + + {Learn more}[link:classes/ActiveRecord/Base.html] + + +* Transaction support on both a database and object level. The latter is implemented + by using Transaction::Simple[http://www.halostatue.ca/ruby/Transaction__Simple.html] + + # Just database transaction + Account.transaction do + david.withdrawal(100) + mary.deposit(100) + end + + # Database and object transaction + Account.transaction(david, mary) do + david.withdrawal(100) + mary.deposit(100) + end + + {Learn more}[link:classes/ActiveRecord/Transactions/ClassMethods.html] + + +* Reflections on columns, associations, and aggregations + + reflection = Firm.reflect_on_association(:clients) + reflection.klass # => Client (class) + Firm.columns # Returns an array of column descriptors for the firms table + + {Learn more}[link:classes/ActiveRecord/Reflection/ClassMethods.html] + + +* Direct manipulation (instead of service invocation) + + So instead of (Hibernate[http://www.hibernate.org/] example): + + long pkId = 1234; + DomesticCat pk = (DomesticCat) sess.load( Cat.class, new Long(pkId) ); + // something interesting involving a cat... + sess.save(cat); + sess.flush(); // force the SQL INSERT + + Active Record lets you: + + pkId = 1234 + cat = Cat.find(pkId) + # something even more interesting involving the same cat... + cat.save + + {Learn more}[link:classes/ActiveRecord/Base.html] + + +* Database abstraction through simple adapters (~100 lines) with a shared connector + + ActiveRecord::Base.establish_connection(:adapter => "sqlite", :database => "dbfile") + + ActiveRecord::Base.establish_connection( + :adapter => "mysql", + :host => "localhost", + :username => "me", + :password => "secret", + :database => "activerecord" + ) + + {Learn more}[link:classes/ActiveRecord/Base.html#M000081] and read about the built-in support for + MySQL[link:classes/ActiveRecord/ConnectionAdapters/MysqlAdapter.html], PostgreSQL[link:classes/ActiveRecord/ConnectionAdapters/PostgreSQLAdapter.html], SQLite[link:classes/ActiveRecord/ConnectionAdapters/SQLiteAdapter.html], Oracle[link:classes/ActiveRecord/ConnectionAdapters/OCIAdapter.html], SQLServer[link:classes/ActiveRecord/ConnectionAdapters/SQLServerAdapter.html], and DB2[link:classes/ActiveRecord/ConnectionAdapters/DB2Adapter.html]. + + +* Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc] + + ActiveRecord::Base.logger = Logger.new(STDOUT) + ActiveRecord::Base.logger = Log4r::Logger.new("Application Log") + + +== Simple example (1/2): Defining tables and classes (using MySQL) + +Data definitions are specified only in the database. Active Record queries the database for +the column names (that then serves to determine which attributes are valid) on regular +object instantiation through the new constructor and relies on the column names in the rows +with the finders. + + # CREATE TABLE companies ( + # id int(11) unsigned NOT NULL auto_increment, + # client_of int(11), + # name varchar(255), + # type varchar(100), + # PRIMARY KEY (id) + # ) + +Active Record automatically links the "Company" object to the "companies" table + + class Company < ActiveRecord::Base + has_many :people, :class_name => "Person" + end + + class Firm < Company + has_many :clients + + def people_with_all_clients + clients.inject([]) { |people, client| people + client.people } + end + end + +The foreign_key is only necessary because we didn't use "firm_id" in the data definition + + class Client < Company + belongs_to :firm, :foreign_key => "client_of" + end + + # CREATE TABLE people ( + # id int(11) unsigned NOT NULL auto_increment, + # name text, + # company_id text, + # PRIMARY KEY (id) + # ) + +Active Record will also automatically link the "Person" object to the "people" table + + class Person < ActiveRecord::Base + belongs_to :company + end + +== Simple example (2/2): Using the domain + +Picking a database connection for all the Active Records + + ActiveRecord::Base.establish_connection( + :adapter => "mysql", + :host => "localhost", + :username => "me", + :password => "secret", + :database => "activerecord" + ) + +Create some fixtures + + firm = Firm.new("name" => "Next Angle") + # SQL: INSERT INTO companies (name, type) VALUES("Next Angle", "Firm") + firm.save + + client = Client.new("name" => "37signals", "client_of" => firm.id) + # SQL: INSERT INTO companies (name, client_of, type) VALUES("37signals", 1, "Firm") + client.save + +Lots of different finders + + # SQL: SELECT * FROM companies WHERE id = 1 + next_angle = Company.find(1) + + # SQL: SELECT * FROM companies WHERE id = 1 AND type = 'Firm' + next_angle = Firm.find(1) + + # SQL: SELECT * FROM companies WHERE id = 1 AND name = 'Next Angle' + next_angle = Company.find_first "name = 'Next Angle'" + + next_angle = Firm.find_by_sql("SELECT * FROM companies WHERE id = 1").first + +The supertype, Company, will return subtype instances + + Firm === next_angle + +All the dynamic methods added by the has_many macro + + next_angle.clients.empty? # true + next_angle.clients.size # total number of clients + all_clients = next_angle.clients + +Constrained finds makes access security easier when ID comes from a web-app + + # SQL: SELECT * FROM companies WHERE client_of = 1 AND type = 'Client' AND id = 2 + thirty_seven_signals = next_angle.clients.find(2) + +Bi-directional associations thanks to the "belongs_to" macro + + thirty_seven_signals.firm.nil? # true + + +== Examples + +Active Record ships with a couple of examples that should give you a good feel for +operating usage. Be sure to edit the examples/shared_setup.rb file for your +own database before running the examples. Possibly also the table definition SQL in +the examples themselves. + +It's also highly recommended to have a look at the unit tests. Read more in link:files/RUNNING_UNIT_TESTS.html + + +== Philosophy + +Active Record attempts to provide a coherent wrapper as a solution for the inconvenience that is +object-relational mapping. The prime directive for this mapping has been to minimize +the amount of code needed to build a real-world domain model. This is made possible +by relying on a number of conventions that make it easy for Active Record to infer +complex relations and structures from a minimal amount of explicit direction. + +Convention over Configuration: +* No XML-files! +* Lots of reflection and run-time extension +* Magic is not inherently a bad word + +Admit the Database: +* Lets you drop down to SQL for odd cases and performance +* Doesn't attempt to duplicate or replace data definitions + + +== Download + +The latest version of Active Record can be found at + +* http://rubyforge.org/project/showfiles.php?group_id=182 + +Documentation can be found at + +* http://ar.rubyonrails.com + + +== Installation + +The prefered method of installing Active Record is through its GEM file. You'll need to have +RubyGems[http://rubygems.rubyforge.org/wiki/wiki.pl] installed for that, though. If you have, +then use: + + % [sudo] gem install activerecord-1.10.0.gem + +You can also install Active Record the old-fashion way with the following command: + + % [sudo] ruby install.rb + +from its distribution directory. + + +== License + +Active Record is released under the MIT license. + + +== Support + +The Active Record homepage is http://www.rubyonrails.com. You can find the Active Record +RubyForge page at http://rubyforge.org/projects/activerecord. And as Jim from Rake says: + + Feel free to submit commits or feature requests. If you send a patch, + remember to update the corresponding unit tests. If fact, I prefer + new feature to be submitted in the form of new unit tests. + +For other information, feel free to ask on the ruby-talk mailing list +(which is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com. diff --git a/vendor/rails/activerecord/RUNNING_UNIT_TESTS b/vendor/rails/activerecord/RUNNING_UNIT_TESTS new file mode 100644 index 00000000..a995bf3b --- /dev/null +++ b/vendor/rails/activerecord/RUNNING_UNIT_TESTS @@ -0,0 +1,46 @@ +== Creating the test database + +The default names for the test databases are "activerecord_unittest" and +"activerecord_unittest2". If you want to use another database name then be sure +to update the connection adapter setups you want to test with in +test/connections//connection.rb. +When you have the database online, you can import the fixture tables with +the test/fixtures/db_definitions/*.sql files. + +Make sure that you create database objects with the same user that you specified in i +connection.rb otherwise (on Postgres, at least) tests for default values will fail. + +== Running with Rake + +The easiest way to run the unit tests is through Rake. The default task runs +the entire test suite for all the adapters. You can also run the suite on just +one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite, +or test_postgresql. For more information, checkout the full array of rake tasks with "rake -T" + +Rake can be found at http://rake.rubyforge.org + +== Running by hand + +Unit tests are located in test directory. If you only want to run a single test suite, +or don't want to bother with Rake, you can do so with something like: + + cd test; ruby -I "connections/native_mysql" base_test.rb + +That'll run the base suite using the MySQL-Ruby adapter. Change the adapter +and test suite name as needed. + +You can also run all the suites on a specific adapter with: + + cd test; all.sh "connections/native_mysql" + +== Faster tests + +If you are using a database that supports transactions, you can set the +"AR_TX_FIXTURES" environment variable to "yes" to use transactional fixtures. +This gives a very large speed boost. With rake: + + rake AR_TX_FIXTURES=yes + +Or, by hand: + + AR_TX_FIXTURES=yes ruby -I connections/native_sqlite3 base_test.rb diff --git a/vendor/rails/activerecord/Rakefile b/vendor/rails/activerecord/Rakefile new file mode 100755 index 00000000..f02c3b5c --- /dev/null +++ b/vendor/rails/activerecord/Rakefile @@ -0,0 +1,181 @@ +require 'rubygems' +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/contrib/rubyforgepublisher' +require File.join(File.dirname(__FILE__), 'lib', 'active_record', 'version') + +PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' +PKG_NAME = 'activerecord' +PKG_VERSION = ActiveRecord::VERSION::STRING + PKG_BUILD +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" + +RELEASE_NAME = "REL #{PKG_VERSION}" + +RUBY_FORGE_PROJECT = "activerecord" +RUBY_FORGE_USER = "webster132" + +PKG_FILES = FileList[ + "lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "[A-Z]*", "install.rb", "Rakefile" +].exclude(/\bCVS\b|~$/) + + +desc "Default Task" +task :default => [ :test_mysql, :test_sqlite, :test_postgresql ] + +# Run the unit tests + +for adapter in %w( mysql postgresql sqlite sqlite3 firebird sqlserver sqlserver_odbc db2 oracle sybase openbase ) + Rake::TestTask.new("test_#{adapter}") { |t| + t.libs << "test" << "test/connections/native_#{adapter}" + t.pattern = "test/*_test{,_#{adapter}}.rb" + t.verbose = true + } +end + +SCHEMA_PATH = File.join(File.dirname(__FILE__), *%w(test fixtures db_definitions)) + +desc 'Build the MySQL test databases' +task :build_mysql_databases do + %x( mysqladmin create activerecord_unittest ) + %x( mysqladmin create activerecord_unittest2 ) + %x( mysql activerecord_unittest < #{File.join(SCHEMA_PATH, 'mysql.sql')} ) + %x( mysql activerecord_unittest < #{File.join(SCHEMA_PATH, 'mysql2.sql')} ) +end + +desc 'Drop the MySQL test databases' +task :drop_mysql_databases do + %x( mysqladmin -f drop activerecord_unittest ) + %x( mysqladmin -f drop activerecord_unittest2 ) +end + +desc 'Rebuild the MySQL test databases' +task :rebuild_mysql_databases => [:drop_mysql_databases, :build_mysql_databases] + +desc 'Build the PostgreSQL test databases' +task :build_postgresql_databases do + %x( createdb activerecord_unittest ) + %x( createdb activerecord_unittest2 ) + %x( psql activerecord_unittest -f #{File.join(SCHEMA_PATH, 'postgresql.sql')} ) + %x( psql activerecord_unittest2 -f #{File.join(SCHEMA_PATH, 'postgresql2.sql')} ) +end + +desc 'Drop the PostgreSQL test databases' +task :drop_postgresql_databases do + %x( dropdb activerecord_unittest ) + %x( dropdb activerecord_unittest2 ) +end + +desc 'Rebuild the PostgreSQL test databases' +task :rebuild_postgresql_databases => [:drop_postgresql_databases, :build_postgresql_databases] + +# Generate the RDoc documentation + +Rake::RDocTask.new { |rdoc| + rdoc.rdoc_dir = 'doc' + rdoc.title = "Active Record -- Object-relation mapping put on rails" + rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' + rdoc.template = "#{ENV['template']}.rb" if ENV['template'] + rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG') + rdoc.rdoc_files.include('lib/**/*.rb') + rdoc.rdoc_files.exclude('lib/active_record/vendor/*') + rdoc.rdoc_files.include('dev-utils/*.rb') +} + +# Enhance rdoc task to copy referenced images also +task :rdoc do + FileUtils.mkdir_p "doc/files/examples/" + FileUtils.copy "examples/associations.png", "doc/files/examples/associations.png" +end + + +# Create compressed packages + +dist_dirs = [ "lib", "test", "examples", "dev-utils" ] + +spec = Gem::Specification.new do |s| + s.name = PKG_NAME + s.version = PKG_VERSION + s.summary = "Implements the ActiveRecord pattern for ORM." + s.description = %q{Implements the ActiveRecord pattern (Fowler, PoEAA) for ORM. It ties database tables and classes together for business objects, like Customer or Subscription, that can find, save, and destroy themselves without resorting to manual SQL.} + + s.files = [ "Rakefile", "install.rb", "README", "RUNNING_UNIT_TESTS", "CHANGELOG" ] + dist_dirs.each do |dir| + s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) } + end + + s.add_dependency('activesupport', '= 1.3.1' + PKG_BUILD) + + s.files.delete "test/fixtures/fixture_database.sqlite" + s.files.delete "test/fixtures/fixture_database_2.sqlite" + s.files.delete "test/fixtures/fixture_database.sqlite3" + s.files.delete "test/fixtures/fixture_database_2.sqlite3" + s.require_path = 'lib' + s.autorequire = 'active_record' + + s.has_rdoc = true + s.extra_rdoc_files = %w( README ) + s.rdoc_options.concat ['--main', 'README'] + + s.author = "David Heinemeier Hansson" + s.email = "david@loudthinking.com" + s.homepage = "http://www.rubyonrails.org" + s.rubyforge_project = "activerecord" +end + +Rake::GemPackageTask.new(spec) do |p| + p.gem_spec = spec + p.need_tar = true + p.need_zip = true +end + +task :lines do + lines, codelines, total_lines, total_codelines = 0, 0, 0, 0 + + for file_name in FileList["lib/active_record/**/*.rb"] + next if file_name =~ /vendor/ + f = File.open(file_name) + + while line = f.gets + lines += 1 + next if line =~ /^\s*$/ + next if line =~ /^\s*#/ + codelines += 1 + end + puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}" + + total_lines += lines + total_codelines += codelines + + lines, codelines = 0, 0 + end + + puts "Total: Lines #{total_lines}, LOC #{total_codelines}" +end + + +# Publishing ------------------------------------------------------ + +desc "Publish the beta gem" +task :pgem => [:package] do + Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload + `ssh davidhh@wrath.rubyonrails.org './gemupdate.sh'` +end + +desc "Publish the API documentation" +task :pdoc => [:rdoc] do + Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/ar", "doc").upload +end + +desc "Publish the release files to RubyForge." +task :release => [ :package ] do + `rubyforge login` + + for ext in %w( gem tgz zip ) + release_command = "rubyforge add_release #{PKG_NAME} #{PKG_NAME} 'REL #{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}" + puts release_command + system(release_command) + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/benchmarks/benchmark.rb b/vendor/rails/activerecord/benchmarks/benchmark.rb new file mode 100644 index 00000000..241d9152 --- /dev/null +++ b/vendor/rails/activerecord/benchmarks/benchmark.rb @@ -0,0 +1,26 @@ +$:.unshift(File.dirname(__FILE__) + '/../lib') +if ARGV[2] + require 'rubygems' + require_gem 'activerecord', ARGV[2] +else + require 'active_record' +end + +ActiveRecord::Base.establish_connection(:adapter => "mysql", :database => "basecamp") + +class Post < ActiveRecord::Base; end + +require 'benchmark' + +RUNS = ARGV[0].to_i +if ARGV[1] == "profile" then require 'profile' end + +runtime = Benchmark::measure { + RUNS.times { + Post.find_all(nil,nil,100).each { |p| p.title } + } +} + +puts "Runs: #{RUNS}" +puts "Avg. runtime: #{runtime.real / RUNS}" +puts "Requests/second: #{RUNS / runtime.real}" diff --git a/vendor/rails/activerecord/benchmarks/mysql_benchmark.rb b/vendor/rails/activerecord/benchmarks/mysql_benchmark.rb new file mode 100644 index 00000000..2f9e0e69 --- /dev/null +++ b/vendor/rails/activerecord/benchmarks/mysql_benchmark.rb @@ -0,0 +1,19 @@ +require 'mysql' + +conn = Mysql::real_connect("localhost", "root", "", "basecamp") + +require 'benchmark' + +require 'profile' if ARGV[1] == "profile" +RUNS = ARGV[0].to_i + +runtime = Benchmark::measure { + RUNS.times { + result = conn.query("SELECT * FROM posts LIMIT 100") + result.each_hash { |p| p["title"] } + } +} + +puts "Runs: #{RUNS}" +puts "Avg. runtime: #{runtime.real / RUNS}" +puts "Requests/second: #{RUNS / runtime.real}" \ No newline at end of file diff --git a/vendor/rails/activerecord/examples/associations.png b/vendor/rails/activerecord/examples/associations.png new file mode 100644 index 00000000..661c7a8b Binary files /dev/null and b/vendor/rails/activerecord/examples/associations.png differ diff --git a/vendor/rails/activerecord/examples/associations.rb b/vendor/rails/activerecord/examples/associations.rb new file mode 100644 index 00000000..b0df3673 --- /dev/null +++ b/vendor/rails/activerecord/examples/associations.rb @@ -0,0 +1,87 @@ +require File.dirname(__FILE__) + '/shared_setup' + +logger = Logger.new(STDOUT) + +# Database setup --------------- + +logger.info "\nCreate tables" + +[ "DROP TABLE companies", "DROP TABLE people", "DROP TABLE people_companies", + "CREATE TABLE companies (id int(11) auto_increment, client_of int(11), name varchar(255), type varchar(100), PRIMARY KEY (id))", + "CREATE TABLE people (id int(11) auto_increment, name varchar(100), PRIMARY KEY (id))", + "CREATE TABLE people_companies (person_id int(11), company_id int(11), PRIMARY KEY (person_id, company_id))", +].each { |statement| + # Tables doesn't necessarily already exist + begin; ActiveRecord::Base.connection.execute(statement); rescue ActiveRecord::StatementInvalid; end +} + + +# Class setup --------------- + +class Company < ActiveRecord::Base + has_and_belongs_to_many :people, :class_name => "Person", :join_table => "people_companies", :table_name => "people" +end + +class Firm < Company + has_many :clients, :foreign_key => "client_of" + + def people_with_all_clients + clients.inject([]) { |people, client| people + client.people } + end +end + +class Client < Company + belongs_to :firm, :foreign_key => "client_of" +end + +class Person < ActiveRecord::Base + has_and_belongs_to_many :companies, :join_table => "people_companies" + def self.table_name() "people" end +end + + +# Usage --------------- + +logger.info "\nCreate fixtures" + +Firm.new("name" => "Next Angle").save +Client.new("name" => "37signals", "client_of" => 1).save +Person.new("name" => "David").save + + +logger.info "\nUsing Finders" + +next_angle = Company.find(1) +next_angle = Firm.find(1) +next_angle = Company.find_first "name = 'Next Angle'" +next_angle = Firm.find_by_sql("SELECT * FROM companies WHERE id = 1").first + +Firm === next_angle + + +logger.info "\nUsing has_many association" + +next_angle.has_clients? +next_angle.clients_count +all_clients = next_angle.clients + +thirty_seven_signals = next_angle.find_in_clients(2) + + +logger.info "\nUsing belongs_to association" + +thirty_seven_signals.has_firm? +thirty_seven_signals.firm?(next_angle) + + +logger.info "\nUsing has_and_belongs_to_many association" + +david = Person.find(1) +david.add_companies(thirty_seven_signals, next_angle) +david.companies.include?(next_angle) +david.companies_count == 2 + +david.remove_companies(next_angle) +david.companies_count == 1 + +thirty_seven_signals.people.include?(david) \ No newline at end of file diff --git a/vendor/rails/activerecord/examples/shared_setup.rb b/vendor/rails/activerecord/examples/shared_setup.rb new file mode 100644 index 00000000..6ede4b1d --- /dev/null +++ b/vendor/rails/activerecord/examples/shared_setup.rb @@ -0,0 +1,15 @@ +# Be sure to change the mysql_connection details and create a database for the example + +$: << File.dirname(__FILE__) + '/../lib' + +require 'active_record' +require 'logger'; class Logger; def format_message(severity, timestamp, msg, progname) "#{msg}\n" end; end + +ActiveRecord::Base.logger = Logger.new(STDOUT) +ActiveRecord::Base.establish_connection( + :adapter => "mysql", + :host => "localhost", + :username => "root", + :password => "", + :database => "activerecord_examples" +) diff --git a/vendor/rails/activerecord/examples/validation.rb b/vendor/rails/activerecord/examples/validation.rb new file mode 100644 index 00000000..e6a448af --- /dev/null +++ b/vendor/rails/activerecord/examples/validation.rb @@ -0,0 +1,85 @@ +require File.dirname(__FILE__) + '/shared_setup' + +logger = Logger.new(STDOUT) + +# Database setup --------------- + +logger.info "\nCreate tables" + +[ "DROP TABLE people", + "CREATE TABLE people (id int(11) auto_increment, name varchar(100), pass varchar(100), email varchar(100), PRIMARY KEY (id))" +].each { |statement| + begin; ActiveRecord::Base.connection.execute(statement); rescue ActiveRecord::StatementInvalid; end # Tables doesn't necessarily already exist +} + + +# Class setup --------------- + +class Person < ActiveRecord::Base + # Using + def self.authenticate(name, pass) + # find_first "name = '#{name}' AND pass = '#{pass}'" would be open to sql-injection (in a web-app scenario) + find_first [ "name = '%s' AND pass = '%s'", name, pass ] + end + + def self.name_exists?(name, id = nil) + if id.nil? + condition = [ "name = '%s'", name ] + else + # Check if anyone else than the person identified by person_id has that user_name + condition = [ "name = '%s' AND id <> %d", name, id ] + end + + !find_first(condition).nil? + end + + def email_address_with_name + "\"#{name}\" <#{email}>" + end + + protected + def validate + errors.add_on_empty(%w(name pass email)) + errors.add("email", "must be valid") unless email_address_valid? + end + + def validate_on_create + if attribute_present?("name") && Person.name_exists?(name) + errors.add("name", "is already taken by another person") + end + end + + def validate_on_update + if attribute_present?("name") && Person.name_exists?(name, id) + errors.add("name", "is already taken by another person") + end + end + + private + def email_address_valid?() email =~ /\w[-.\w]*\@[-\w]+[-.\w]*\.\w+/ end +end + +# Usage --------------- + +logger.info "\nCreate fixtures" +david = Person.new("name" => "David Heinemeier Hansson", "pass" => "", "email" => "") +unless david.save + puts "There was #{david.errors.count} error(s)" + david.errors.each_full { |error| puts error } +end + +david.pass = "something" +david.email = "invalid_address" +unless david.save + puts "There was #{david.errors.count} error(s)" + puts "It was email with: " + david.errors.on("email") +end + +david.email = "david@loudthinking.com" +if david.save then puts "David finally made it!" end + + +another_david = Person.new("name" => "David Heinemeier Hansson", "pass" => "xc", "email" => "david@loudthinking") +unless another_david.save + puts "Error on name: " + another_david.errors.on("name") +end \ No newline at end of file diff --git a/vendor/rails/activerecord/install.rb b/vendor/rails/activerecord/install.rb new file mode 100644 index 00000000..592c4b9d --- /dev/null +++ b/vendor/rails/activerecord/install.rb @@ -0,0 +1,30 @@ +require 'rbconfig' +require 'find' +require 'ftools' + +include Config + +# this was adapted from rdoc's install.rb by ways of Log4r + +$sitedir = CONFIG["sitelibdir"] +unless $sitedir + version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"] + $libdir = File.join(CONFIG["libdir"], "ruby", version) + $sitedir = $:.find {|x| x =~ /site_ruby/ } + if !$sitedir + $sitedir = File.join($libdir, "site_ruby") + elsif $sitedir !~ Regexp.quote(version) + $sitedir = File.join($sitedir, version) + end +end + +# the acual gruntwork +Dir.chdir("lib") + +Find.find("active_record", "active_record.rb") { |f| + if f[-3..-1] == ".rb" + File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true) + else + File::makedirs(File.join($sitedir, *f.split(/\//))) + end +} diff --git a/vendor/rails/activerecord/lib/active_record.rb b/vendor/rails/activerecord/lib/active_record.rb new file mode 100755 index 00000000..293c86f1 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record.rb @@ -0,0 +1,79 @@ +#-- +# Copyright (c) 2004 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +$:.unshift(File.dirname(__FILE__)) unless + $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) + +unless defined?(ActiveSupport) + begin + $:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib") + require 'active_support' + rescue LoadError + require 'rubygems' + require_gem 'activesupport' + end +end + +require 'active_record/base' +require 'active_record/observer' +require 'active_record/validations' +require 'active_record/callbacks' +require 'active_record/reflection' +require 'active_record/associations' +require 'active_record/aggregations' +require 'active_record/transactions' +require 'active_record/timestamp' +require 'active_record/acts/list' +require 'active_record/acts/tree' +require 'active_record/acts/nested_set' +require 'active_record/locking' +require 'active_record/migration' +require 'active_record/schema' +require 'active_record/calculations' + +ActiveRecord::Base.class_eval do + include ActiveRecord::Validations + include ActiveRecord::Locking + include ActiveRecord::Callbacks + include ActiveRecord::Observing + include ActiveRecord::Timestamp + include ActiveRecord::Associations + include ActiveRecord::Aggregations + include ActiveRecord::Transactions + include ActiveRecord::Reflection + include ActiveRecord::Acts::Tree + include ActiveRecord::Acts::List + include ActiveRecord::Acts::NestedSet + include ActiveRecord::Calculations +end + +unless defined?(RAILS_CONNECTION_ADAPTERS) + RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase ) +end + +RAILS_CONNECTION_ADAPTERS.each do |adapter| + require "active_record/connection_adapters/" + adapter + "_adapter" +end + +require 'active_record/query_cache' +require 'active_record/schema_dumper' diff --git a/vendor/rails/activerecord/lib/active_record/acts/list.rb b/vendor/rails/activerecord/lib/active_record/acts/list.rb new file mode 100644 index 00000000..0e0e1e4f --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/acts/list.rb @@ -0,0 +1,233 @@ +module ActiveRecord + module Acts #:nodoc: + module List #:nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + end + + # This act provides the capabilities for sorting and reordering a number of objects in a list. + # The class that has this specified needs to have a "position" column defined as an integer on + # the mapped database table. + # + # Todo list example: + # + # class TodoList < ActiveRecord::Base + # has_many :todo_items, :order => "position" + # end + # + # class TodoItem < ActiveRecord::Base + # belongs_to :todo_list + # acts_as_list :scope => :todo_list + # end + # + # todo_list.first.move_to_bottom + # todo_list.last.move_higher + module ClassMethods + # Configuration options are: + # + # * +column+ - specifies the column name to use for keeping the position integer (default: position) + # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id" + # (if that hasn't been already) and use that as the foreign key restriction. It's also possible + # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key. + # Example: acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0' + def acts_as_list(options = {}) + configuration = { :column => "position", :scope => "1 = 1" } + configuration.update(options) if options.is_a?(Hash) + + configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/ + + if configuration[:scope].is_a?(Symbol) + scope_condition_method = %( + def scope_condition + if #{configuration[:scope].to_s}.nil? + "#{configuration[:scope].to_s} IS NULL" + else + "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}" + end + end + ) + else + scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end" + end + + class_eval <<-EOV + include ActiveRecord::Acts::List::InstanceMethods + + def acts_as_list_class + ::#{self.name} + end + + def position_column + '#{configuration[:column]}' + end + + #{scope_condition_method} + + after_destroy :remove_from_list + before_create :add_to_list_bottom + EOV + end + end + + # All the methods available to a record that has had acts_as_list specified. Each method works + # by assuming the object to be the item in the list, so chapter.move_lower would move that chapter + # lower in the list of all chapters. Likewise, chapter.first? would return true if that chapter is + # the first in the list of all chapters. + module InstanceMethods + def insert_at(position = 1) + insert_at_position(position) + end + + def move_lower + return unless lower_item + + acts_as_list_class.transaction do + lower_item.decrement_position + increment_position + end + end + + def move_higher + return unless higher_item + + acts_as_list_class.transaction do + higher_item.increment_position + decrement_position + end + end + + def move_to_bottom + return unless in_list? + acts_as_list_class.transaction do + decrement_positions_on_lower_items + assume_bottom_position + end + end + + def move_to_top + return unless in_list? + acts_as_list_class.transaction do + increment_positions_on_higher_items + assume_top_position + end + end + + def remove_from_list + decrement_positions_on_lower_items if in_list? + end + + def increment_position + return unless in_list? + update_attribute position_column, self.send(position_column).to_i + 1 + end + + def decrement_position + return unless in_list? + update_attribute position_column, self.send(position_column).to_i - 1 + end + + def first? + return false unless in_list? + self.send(position_column) == 1 + end + + def last? + return false unless in_list? + self.send(position_column) == bottom_position_in_list + end + + def higher_item + return nil unless in_list? + acts_as_list_class.find(:first, :conditions => + "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}" + ) + end + + def lower_item + return nil unless in_list? + acts_as_list_class.find(:first, :conditions => + "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}" + ) + end + + def in_list? + !send(position_column).nil? + end + + private + def add_to_list_top + increment_positions_on_all_items + end + + def add_to_list_bottom + self[position_column] = bottom_position_in_list.to_i + 1 + end + + # Overwrite this method to define the scope of the list changes + def scope_condition() "1" end + + def bottom_position_in_list(except = nil) + item = bottom_item(except) + item ? item.send(position_column) : 0 + end + + def bottom_item(except = nil) + conditions = scope_condition + conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except + acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC") + end + + def assume_bottom_position + update_attribute(position_column, bottom_position_in_list(self).to_i + 1) + end + + def assume_top_position + update_attribute(position_column, 1) + end + + # This has the effect of moving all the higher items up one. + def decrement_positions_on_higher_items(position) + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}" + ) + end + + # This has the effect of moving all the lower items up one. + def decrement_positions_on_lower_items + return unless in_list? + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}" + ) + end + + # This has the effect of moving all the higher items down one. + def increment_positions_on_higher_items + return unless in_list? + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}" + ) + end + + # This has the effect of moving all the lower items down one. + def increment_positions_on_lower_items(position) + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}" + ) + end + + def increment_positions_on_all_items + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} + 1)", "#{scope_condition}" + ) + end + + def insert_at_position(position) + remove_from_list + increment_positions_on_lower_items(position) + self.update_attribute(position_column, position) + end + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/acts/nested_set.rb b/vendor/rails/activerecord/lib/active_record/acts/nested_set.rb new file mode 100644 index 00000000..8b02b358 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/acts/nested_set.rb @@ -0,0 +1,212 @@ +module ActiveRecord + module Acts #:nodoc: + module NestedSet #:nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + end + + # This acts provides Nested Set functionality. Nested Set is similiar to Tree, but with + # the added feature that you can select the children and all of their descendents with + # a single query. A good use case for this is a threaded post system, where you want + # to display every reply to a comment without multiple selects. + # + # A google search for "Nested Set" should point you in the direction to explain the + # database theory. I figured out a bunch of this from + # http://threebit.net/tutorials/nestedset/tutorial1.html + # + # Instead of picturing a leaf node structure with children pointing back to their parent, + # the best way to imagine how this works is to think of the parent entity surrounding all + # of its children, and its parent surrounding it, etc. Assuming that they are lined up + # horizontally, we store the left and right boundries in the database. + # + # Imagine: + # root + # |_ Child 1 + # |_ Child 1.1 + # |_ Child 1.2 + # |_ Child 2 + # |_ Child 2.1 + # |_ Child 2.2 + # + # If my cirlces in circles description didn't make sense, check out this sweet + # ASCII art: + # + # ___________________________________________________________________ + # | Root | + # | ____________________________ ____________________________ | + # | | Child 1 | | Child 2 | | + # | | __________ _________ | | __________ _________ | | + # | | | C 1.1 | | C 1.2 | | | | C 2.1 | | C 2.2 | | | + # 1 2 3_________4 5________6 7 8 9_________10 11_______12 13 14 + # | |___________________________| |___________________________| | + # |___________________________________________________________________| + # + # The numbers represent the left and right boundries. The table then might + # look like this: + # ID | PARENT | LEFT | RIGHT | DATA + # 1 | 0 | 1 | 14 | root + # 2 | 1 | 2 | 7 | Child 1 + # 3 | 2 | 3 | 4 | Child 1.1 + # 4 | 2 | 5 | 6 | Child 1.2 + # 5 | 1 | 8 | 13 | Child 2 + # 6 | 5 | 9 | 10 | Child 2.1 + # 7 | 5 | 11 | 12 | Child 2.2 + # + # So, to get all children of an entry, you + # SELECT * WHERE CHILD.LEFT IS BETWEEN PARENT.LEFT AND PARENT.RIGHT + # + # To get the count, it's (LEFT - RIGHT + 1)/2, etc. + # + # To get the direct parent, it falls back to using the PARENT_ID field. + # + # There are instance methods for all of these. + # + # The structure is good if you need to group things together; the downside is that + # keeping data integrity is a pain, and both adding and removing an entry + # require a full table write. + # + # This sets up a before_destroy trigger to prune the tree correctly if one of its + # elements gets deleted. + # + module ClassMethods + # Configuration options are: + # + # * +parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id) + # * +left_column+ - column name for left boundry data, default "lft" + # * +right_column+ - column name for right boundry data, default "rgt" + # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id" + # (if that hasn't been already) and use that as the foreign key restriction. It's also possible + # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key. + # Example: acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0' + def acts_as_nested_set(options = {}) + configuration = { :parent_column => "parent_id", :left_column => "lft", :right_column => "rgt", :scope => "1 = 1" } + + configuration.update(options) if options.is_a?(Hash) + + configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/ + + if configuration[:scope].is_a?(Symbol) + scope_condition_method = %( + def scope_condition + if #{configuration[:scope].to_s}.nil? + "#{configuration[:scope].to_s} IS NULL" + else + "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}" + end + end + ) + else + scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end" + end + + class_eval <<-EOV + include ActiveRecord::Acts::NestedSet::InstanceMethods + + #{scope_condition_method} + + def left_col_name() "#{configuration[:left_column]}" end + + def right_col_name() "#{configuration[:right_column]}" end + + def parent_column() "#{configuration[:parent_column]}" end + + EOV + end + end + + module InstanceMethods + # Returns true is this is a root node. + def root? + parent_id = self[parent_column] + (parent_id == 0 || parent_id.nil?) && (self[left_col_name] == 1) && (self[right_col_name] > self[left_col_name]) + end + + # Returns true is this is a child node + def child? + parent_id = self[parent_column] + !(parent_id == 0 || parent_id.nil?) && (self[left_col_name] > 1) && (self[right_col_name] > self[left_col_name]) + end + + # Returns true if we have no idea what this is + def unknown? + !root? && !child? + end + + + # Adds a child to this object in the tree. If this object hasn't been initialized, + # it gets set up as a root node. Otherwise, this method will update all of the + # other elements in the tree and shift them to the right, keeping everything + # balanced. + def add_child( child ) + self.reload + child.reload + + if child.root? + raise "Adding sub-tree isn\'t currently supported" + else + if ( (self[left_col_name] == nil) || (self[right_col_name] == nil) ) + # Looks like we're now the root node! Woo + self[left_col_name] = 1 + self[right_col_name] = 4 + + # What do to do about validation? + return nil unless self.save + + child[parent_column] = self.id + child[left_col_name] = 2 + child[right_col_name]= 3 + return child.save + else + # OK, we need to add and shift everything else to the right + child[parent_column] = self.id + right_bound = self[right_col_name] + child[left_col_name] = right_bound + child[right_col_name] = right_bound + 1 + self[right_col_name] += 2 + self.class.transaction { + self.class.update_all( "#{left_col_name} = (#{left_col_name} + 2)", "#{scope_condition} AND #{left_col_name} >= #{right_bound}" ) + self.class.update_all( "#{right_col_name} = (#{right_col_name} + 2)", "#{scope_condition} AND #{right_col_name} >= #{right_bound}" ) + self.save + child.save + } + end + end + end + + # Returns the number of nested children of this object. + def children_count + return (self[right_col_name] - self[left_col_name] - 1)/2 + end + + # Returns a set of itself and all of its nested children + def full_set + self.class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} and #{self[right_col_name]})" ) + end + + # Returns a set of all of its children and nested children + def all_children + self.class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} > #{self[left_col_name]}) and (#{right_col_name} < #{self[right_col_name]})" ) + end + + # Returns a set of only this entry's immediate children + def direct_children + self.class.find(:all, :conditions => "#{scope_condition} and #{parent_column} = #{self.id}") + end + + # Prunes a branch off of the tree, shifting all of the elements on the right + # back to the left so the counts still work. + def before_destroy + return if self[right_col_name].nil? || self[left_col_name].nil? + dif = self[right_col_name] - self[left_col_name] + 1 + + self.class.transaction { + self.class.delete_all( "#{scope_condition} and #{left_col_name} > #{self[left_col_name]} and #{right_col_name} < #{self[right_col_name]}" ) + self.class.update_all( "#{left_col_name} = (#{left_col_name} - #{dif})", "#{scope_condition} AND #{left_col_name} >= #{self[right_col_name]}" ) + self.class.update_all( "#{right_col_name} = (#{right_col_name} - #{dif} )", "#{scope_condition} AND #{right_col_name} >= #{self[right_col_name]}" ) + } + end + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/acts/tree.rb b/vendor/rails/activerecord/lib/active_record/acts/tree.rb new file mode 100644 index 00000000..c5aa4cd2 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/acts/tree.rb @@ -0,0 +1,90 @@ +module ActiveRecord + module Acts #:nodoc: + module Tree #:nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + end + + # Specify this act if you want to model a tree structure by providing a parent association and a children + # association. This act requires that you have a foreign key column, which by default is called parent_id. + # + # class Category < ActiveRecord::Base + # acts_as_tree :order => "name" + # end + # + # Example : + # root + # \_ child1 + # \_ subchild1 + # \_ subchild2 + # + # root = Category.create("name" => "root") + # child1 = root.children.create("name" => "child1") + # subchild1 = child1.children.create("name" => "subchild1") + # + # root.parent # => nil + # child1.parent # => root + # root.children # => [child1] + # root.children.first.children.first # => subchild1 + # + # In addition to the parent and children associations, the following instance methods are added to the class + # after specifying the act: + # * siblings : Returns all the children of the parent, excluding the current node ([ subchild2 ] when called from subchild1) + # * self_and_siblings : Returns all the children of the parent, including the current node ([ subchild1, subchild2 ] when called from subchild1) + # * ancestors : Returns all the ancestors of the current node ([child1, root] when called from subchild2) + # * root : Returns the root of the current node (root when called from subchild2) + module ClassMethods + # Configuration options are: + # + # * foreign_key - specifies the column name to use for tracking of the tree (default: parent_id) + # * order - makes it possible to sort the children according to this SQL snippet. + # * counter_cache - keeps a count in a children_count column if set to true (default: false). + def acts_as_tree(options = {}) + configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil } + configuration.update(options) if options.is_a?(Hash) + + belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache] + has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => :destroy + + class_eval <<-EOV + include ActiveRecord::Acts::Tree::InstanceMethods + + def self.roots + find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}}) + end + + def self.root + find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}}) + end + EOV + end + end + + module InstanceMethods + # Returns list of ancestors, starting from parent until root. + # + # subchild1.ancestors # => [child1, root] + def ancestors + node, nodes = self, [] + nodes << node = node.parent until not node.has_parent? + nodes + end + + def root + node = self + node = node.parent until not node.has_parent? + node + end + + def siblings + self_and_siblings - [self] + end + + def self_and_siblings + has_parent? ? parent.children : self.class.roots + end + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/aggregations.rb b/vendor/rails/activerecord/lib/active_record/aggregations.rb new file mode 100644 index 00000000..314c40cf --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/aggregations.rb @@ -0,0 +1,167 @@ +module ActiveRecord + module Aggregations # :nodoc: + def self.included(base) + base.extend(ClassMethods) + end + + def clear_aggregation_cache #:nodoc: + self.class.reflect_on_all_aggregations.to_a.each do |assoc| + instance_variable_set "@#{assoc.name}", nil + end unless self.new_record? + end + + # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes + # as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is] + # composed of [an] address". Each call to the macro adds a description of how the value objects are created from the + # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object) + # and how it can be turned back into attributes (when the entity is saved to the database). Example: + # + # class Customer < ActiveRecord::Base + # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount) + # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ] + # end + # + # The customer class now has the following methods to manipulate the value objects: + # * Customer#balance, Customer#balance=(money) + # * Customer#address, Customer#address=(address) + # + # These methods will operate with value objects like the ones described below: + # + # class Money + # include Comparable + # attr_reader :amount, :currency + # EXCHANGE_RATES = { "USD_TO_DKK" => 6 } + # + # def initialize(amount, currency = "USD") + # @amount, @currency = amount, currency + # end + # + # def exchange_to(other_currency) + # exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor + # Money.new(exchanged_amount, other_currency) + # end + # + # def ==(other_money) + # amount == other_money.amount && currency == other_money.currency + # end + # + # def <=>(other_money) + # if currency == other_money.currency + # amount <=> amount + # else + # amount <=> other_money.exchange_to(currency).amount + # end + # end + # end + # + # class Address + # attr_reader :street, :city + # def initialize(street, city) + # @street, @city = street, city + # end + # + # def close_to?(other_address) + # city == other_address.city + # end + # + # def ==(other_address) + # city == other_address.city && street == other_address.street + # end + # end + # + # Now it's possible to access attributes from the database through the value objects instead. If you choose to name the + # composition the same as the attributes name, it will be the only way to access that attribute. That's the case with our + # +balance+ attribute. You interact with the value objects just like you would any other attribute, though: + # + # customer.balance = Money.new(20) # sets the Money value object and the attribute + # customer.balance # => Money value object + # customer.balance.exchanged_to("DKK") # => Money.new(120, "DKK") + # customer.balance > Money.new(10) # => true + # customer.balance == Money.new(20) # => true + # customer.balance < Money.new(5) # => false + # + # Value objects can also be composed of multiple attributes, such as the case of Address. The order of the mappings will + # determine the order of the parameters. Example: + # + # customer.address_street = "Hyancintvej" + # customer.address_city = "Copenhagen" + # customer.address # => Address.new("Hyancintvej", "Copenhagen") + # customer.address = Address.new("May Street", "Chicago") + # customer.address_street # => "May Street" + # customer.address_city # => "Chicago" + # + # == Writing value objects + # + # Value objects are immutable and interchangeable objects that represent a given value, such as a Money object representing + # $5. Two Money objects both representing $5 should be equal (through methods such as == and <=> from Comparable if ranking + # makes sense). This is unlike entity objects where equality is determined by identity. An entity class such as Customer can + # easily have two different objects that both have an address on Hyancintvej. Entity identity is determined by object or + # relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects. + # + # It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after + # creation. Create a new money object with the new value instead. This is exemplified by the Money#exchanged_to method that + # returns a new value object instead of changing its own values. Active Record won't persist value objects that have been + # changed through other means than the writer method. + # + # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to + # change it afterwards will result in a TypeError. + # + # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects + # immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable + module ClassMethods + # Adds the a reader and writer method for manipulating a value object, so + # composed_of :address would add address and address=(new_address). + # + # Options are: + # * :class_name - specify the class name of the association. Use it only if that name can't be inferred + # from the part id. So composed_of :address will by default be linked to the +Address+ class, but + # if the real class name is +CompanyAddress+, you'll have to specify it with this option. + # * :mapping - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name + # to a constructor parameter on the value class. + # + # Option examples: + # composed_of :temperature, :mapping => %w(reading celsius) + # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount) + # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ] + # composed_of :gps_location + def composed_of(part_id, options = {}) + options.assert_valid_keys(:class_name, :mapping) + + name = part_id.id2name + class_name = options[:class_name] || name_to_class_name(name) + mapping = options[:mapping] || [ name, name ] + + reader_method(name, class_name, mapping) + writer_method(name, class_name, mapping) + + create_reflection(:composed_of, part_id, options, self) + end + + private + def name_to_class_name(name) + name.capitalize.gsub(/_(.)/) { |s| $1.capitalize } + end + + def reader_method(name, class_name, mapping) + module_eval <<-end_eval + def #{name}(force_reload = false) + if @#{name}.nil? || force_reload + @#{name} = #{class_name}.new(#{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "read_attribute(\"#{pair.first}\")"}.join(", ")}) + end + + return @#{name} + end + end_eval + end + + def writer_method(name, class_name, mapping) + module_eval <<-end_eval + def #{name}=(part) + @#{name} = part.freeze + #{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "@attributes[\"#{pair.first}\"] = part.#{pair.last}" }.join("\n")} + end + end_eval + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/associations.rb b/vendor/rails/activerecord/lib/active_record/associations.rb new file mode 100755 index 00000000..ea497d5c --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/associations.rb @@ -0,0 +1,1559 @@ +require 'active_record/associations/association_proxy' +require 'active_record/associations/association_collection' +require 'active_record/associations/belongs_to_association' +require 'active_record/associations/belongs_to_polymorphic_association' +require 'active_record/associations/has_one_association' +require 'active_record/associations/has_many_association' +require 'active_record/associations/has_many_through_association' +require 'active_record/associations/has_and_belongs_to_many_association' +require 'active_record/deprecated_associations' + +module ActiveRecord + class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc: + def initialize(reflection) + @reflection = reflection + end + + def message + "Could not find the association #{@reflection.options[:through].inspect} in model #{@reflection.klass}" + end + end + + class HasManyThroughAssociationPolymorphicError < ActiveRecordError #:nodoc: + def initialize(owner_class_name, reflection, source_reflection) + @owner_class_name = owner_class_name + @reflection = reflection + @source_reflection = source_reflection + end + + def message + "Cannot have a has_many :through association '#{@owner_class_name}##{@reflection.name}' on the polymorphic object '#{@source_reflection.class_name}##{@source_reflection.name}'." + end + end + + class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc: + def initialize(reflection) + @reflection = reflection + @through_reflection = reflection.through_reflection + @source_reflection_names = reflection.source_reflection_names + @source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect } + end + + def message + "Could not find the source association(s) #{@source_reflection_names.collect(&:inspect).to_sentence :connector => 'or'} in model #{@through_reflection.klass}. Try 'has_many #{@reflection.name.inspect}, :through => #{@through_reflection.name.inspect}, :source => '. Is it one of #{@source_associations.to_sentence :connector => 'or'}?" + end + end + + class HasManyThroughSourceAssociationMacroError < ActiveRecordError #:nodoc + def initialize(reflection) + @reflection = reflection + @through_reflection = reflection.through_reflection + @source_reflection = reflection.source_reflection + end + + def message + "Invalid source reflection macro :#{@source_reflection.macro}#{" :through" if @source_reflection.options[:through]} for has_many #{@reflection.name.inspect}, :through => #{@through_reflection.name.inspect}. Use :source to specify the source reflection." + end + end + + class EagerLoadPolymorphicError < ActiveRecordError #:nodoc: + def initialize(reflection) + @reflection = reflection + end + + def message + "Can not eagerly load the polymorphic association #{@reflection.name.inspect}" + end + end + + module Associations # :nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + end + + # Clears out the association cache + def clear_association_cache #:nodoc: + self.class.reflect_on_all_associations.to_a.each do |assoc| + instance_variable_set "@#{assoc.name}", nil + end unless self.new_record? + end + + # Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like + # "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are + # specialized according to the collection or association symbol and the options hash. It works much the same way as Ruby's own attr* + # methods. Example: + # + # class Project < ActiveRecord::Base + # belongs_to :portfolio + # has_one :project_manager + # has_many :milestones + # has_and_belongs_to_many :categories + # end + # + # The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships: + # * Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil? + # * Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?, + # * Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone), + # Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find_all(conditions), + # Project#milestones.build, Project#milestones.create + # * Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1), + # Project#categories.delete(category1) + # + # == Example + # + # link:files/examples/associations.png + # + # == Is it belongs_to or has_one? + # + # Both express a 1-1 relationship, the difference is mostly where to place the foreign key, which goes on the table for the class + # saying belongs_to. Example: + # + # class Post < ActiveRecord::Base + # has_one :author + # end + # + # class Author < ActiveRecord::Base + # belongs_to :post + # end + # + # The tables for these classes could look something like: + # + # CREATE TABLE posts ( + # id int(11) NOT NULL auto_increment, + # title varchar default NULL, + # PRIMARY KEY (id) + # ) + # + # CREATE TABLE authors ( + # id int(11) NOT NULL auto_increment, + # post_id int(11) default NULL, + # name varchar default NULL, + # PRIMARY KEY (id) + # ) + # + # == Unsaved objects and associations + # + # You can manipulate objects and associations before they are saved to the database, but there is some special behaviour you should be + # aware of, mostly involving the saving of associated objects. + # + # === One-to-one associations + # + # * Assigning an object to a has_one association automatically saves that object and the object being replaced (if there is one), in + # order to update their primary keys - except if the parent object is unsaved (new_record? == true). + # * If either of these saves fail (due to one of the objects being invalid) the assignment statement returns false and the assignment + # is cancelled. + # * If you wish to assign an object to a has_one association without saving it, use the #association.build method (documented below). + # * Assigning an object to a belongs_to association does not save the object, since the foreign key field belongs on the parent. It does + # not save the parent either. + # + # === Collections + # + # * Adding an object to a collection (has_many or has_and_belongs_to_many) automatically saves that object, except if the parent object + # (the owner of the collection) is not yet stored in the database. + # * If saving any of the objects being added to a collection (via #push or similar) fails, then #push returns false. + # * You can add an object to a collection without automatically saving it by using the #collection.build method (documented below). + # * All unsaved (new_record? == true) members of the collection are automatically saved when the parent is saved. + # + # === Association callbacks + # + # Similiar to the normal callbacks that hook into the lifecycle of an Active Record object, you can also define callbacks that get + # trigged when you add an object to or removing an object from a association collection. Example: + # + # class Project + # has_and_belongs_to_many :developers, :after_add => :evaluate_velocity + # + # def evaluate_velocity(developer) + # ... + # end + # end + # + # It's possible to stack callbacks by passing them as an array. Example: + # + # class Project + # has_and_belongs_to_many :developers, :after_add => [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}] + # end + # + # Possible callbacks are: before_add, after_add, before_remove and after_remove. + # + # Should any of the before_add callbacks throw an exception, the object does not get added to the collection. Same with + # the before_remove callbacks, if an exception is thrown the object doesn't get removed. + # + # === Association extensions + # + # The proxy objects that controls the access to associations can be extended through anonymous modules. This is especially + # beneficial for adding new finders, creators, and other factory-type methods that are only used as part of this association. + # Example: + # + # class Account < ActiveRecord::Base + # has_many :people do + # def find_or_create_by_name(name) + # first_name, last_name = name.split(" ", 2) + # find_or_create_by_first_name_and_last_name(first_name, last_name) + # end + # end + # end + # + # person = Account.find(:first).people.find_or_create_by_name("David Heinemeier Hansson") + # person.first_name # => "David" + # person.last_name # => "Heinemeier Hansson" + # + # If you need to share the same extensions between many associations, you can use a named extension module. Example: + # + # module FindOrCreateByNameExtension + # def find_or_create_by_name(name) + # first_name, last_name = name.split(" ", 2) + # find_or_create_by_first_name_and_last_name(first_name, last_name) + # end + # end + # + # class Account < ActiveRecord::Base + # has_many :people, :extend => FindOrCreateByNameExtension + # end + # + # class Company < ActiveRecord::Base + # has_many :people, :extend => FindOrCreateByNameExtension + # end + # + # === Association Join Models + # + # Has Many associations can be configured with the :through option to use an explicit join model to retrieve the data. This + # operates similarly to a has_and_belongs_to_many association. The advantage is that you're able to add validations, + # callbacks, and extra attributes on the join model. Consider the following schema: + # + # class Author < ActiveRecord::Base + # has_many :authorships + # has_many :books, :through => :authorships + # end + # + # class Authorship < ActiveRecord::Base + # belongs_to :author + # belongs_to :book + # end + # + # @author = Author.find :first + # @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to. + # @author.books # selects all books by using the Authorship join model + # + # You can also go through a has_many association on the join model: + # + # class Firm < ActiveRecord::Base + # has_many :clients + # has_many :invoices, :through => :clients + # end + # + # class Client < ActiveRecord::Base + # belongs_to :firm + # has_many :invoices + # end + # + # class Invoice < ActiveRecord::Base + # belongs_to :client + # end + # + # @firm = Firm.find :first + # @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm + # @firm.invoices # selects all invoices by going through the Client join model. + # + # === Polymorphic Associations + # + # Polymorphic associations on models are not restricted on what types of models they can be associated with. Rather, they + # specify an interface that a has_many association must adhere to. + # + # class Asset < ActiveRecord::Base + # belongs_to :attachable, :polymorphic => true + # end + # + # class Post < ActiveRecord::Base + # has_many :assets, :as => :attachable # The :as option specifies the polymorphic interface to use. + # end + # + # @asset.attachable = @post + # + # This works by using a type column in addition to a foreign key to specify the associated record. In the Asset example, you'd need + # an attachable_id integer column and an attachable_type string column. + # + # == Caching + # + # All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically + # instructed not to. The cache is even shared across methods to make it even cheaper to use the macro-added methods without + # worrying too much about performance at the first go. Example: + # + # project.milestones # fetches milestones from the database + # project.milestones.size # uses the milestone cache + # project.milestones.empty? # uses the milestone cache + # project.milestones(true).size # fetches milestones from the database + # project.milestones # uses the milestone cache + # + # == Eager loading of associations + # + # Eager loading is a way to find objects of a certain class and a number of named associations along with it in a single SQL call. This is + # one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 posts that each needs to display their author + # triggers 101 database queries. Through the use of eager loading, the 101 queries can be reduced to 1. Example: + # + # class Post < ActiveRecord::Base + # belongs_to :author + # has_many :comments + # end + # + # Consider the following loop using the class above: + # + # for post in Post.find(:all) + # puts "Post: " + post.title + # puts "Written by: " + post.author.name + # puts "Last comment on: " + post.comments.first.created_on + # end + # + # To iterate over these one hundred posts, we'll generate 201 database queries. Let's first just optimize it for retrieving the author: + # + # for post in Post.find(:all, :include => :author) + # + # This references the name of the belongs_to association that also used the :author symbol, so the find will now weave in a join something + # like this: LEFT OUTER JOIN authors ON authors.id = posts.author_id. Doing so will cut down the number of queries from 201 to 101. + # + # We can improve upon the situation further by referencing both associations in the finder with: + # + # 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. + # 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 + # 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. + # + # Please note that limited eager loading with has_many and has_and_belongs_to_many associations is not compatible with describing conditions + # on these eager tables. This will work: + # + # Post.find(:all, :include => :comments, :conditions => "posts.title = 'magic forest'", :limit => 2) + # + # ...but this will not (and an ArgumentError will be raised): + # + # Post.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%'", :limit => 2) + # + # Also have in mind that since the eager loading is pulling from multiple tables, you'll have to disambiguate any column references + # in both conditions and orders. So :order => "posts.id DESC" will work while :order => "id DESC" will not. This may require that + # you alter the :order and :conditions on the association definitions themselves. + # + # It's currently not possible to use eager loading on multiple associations from the same table. Eager loading will not pull + # additional attributes on join tables, so "rich associations" with has_and_belongs_to_many is not a good fit for eager loading. + # + # == Table Aliasing + # + # ActiveRecord uses table aliasing in the case that a table is referenced multiple times in a join. If a table is referenced only once, + # the standard table name is used. The second time, the table is aliased as #{reflection_name}_#{parent_table_name}. Indexes are appended + # for any more successive uses of the table name. + # + # Post.find :all, :include => :comments + # # => SELECT ... FROM posts LEFT OUTER JOIN comments ON ... + # Post.find :all, :include => :special_comments # STI + # # => SELECT ... FROM posts LEFT OUTER JOIN comments ON ... AND comments.type = 'SpecialComment' + # Post.find :all, :include => [:comments, :special_comments] # special_comments is the reflection name, posts is the parent table name + # # => SELECT ... FROM posts LEFT OUTER JOIN comments ON ... LEFT OUTER JOIN comments special_comments_posts + # + # Acts as tree example: + # + # TreeMixin.find :all, :include => :children + # # => SELECT ... FROM mixins LEFT OUTER JOIN mixins childrens_mixins ... + # TreeMixin.find :all, :include => {:children => :parent} # using cascading eager includes + # # => SELECT ... FROM mixins LEFT OUTER JOIN mixins childrens_mixins ... + # LEFT OUTER JOIN parents_mixins ... + # TreeMixin.find :all, :include => {:children => {:parent => :children}} + # # => SELECT ... FROM mixins LEFT OUTER JOIN mixins childrens_mixins ... + # LEFT OUTER JOIN parents_mixins ... + # LEFT OUTER JOIN mixins childrens_mixins_2 + # + # Has and Belongs to Many join tables use the same idea, but add a _join suffix: + # + # Post.find :all, :include => :categories + # # => SELECT ... FROM posts LEFT OUTER JOIN categories_posts ... LEFT OUTER JOIN categories ... + # Post.find :all, :include => {:categories => :posts} + # # => SELECT ... FROM posts LEFT OUTER JOIN categories_posts ... LEFT OUTER JOIN categories ... + # LEFT OUTER JOIN categories_posts posts_categories_join LEFT OUTER JOIN posts posts_categories + # Post.find :all, :include => {:categories => {:posts => :categories}} + # # => SELECT ... FROM posts LEFT OUTER JOIN categories_posts ... LEFT OUTER JOIN categories ... + # LEFT OUTER JOIN categories_posts posts_categories_join LEFT OUTER JOIN posts posts_categories + # LEFT OUTER JOIN categories_posts categories_posts_join LEFT OUTER JOIN categories categories_posts + # + # If you wish to specify your own custom joins using a :joins option, those table names will take precedence over the eager associations.. + # + # Post.find :all, :include => :comments, :joins => "inner join comments ..." + # # => SELECT ... FROM posts LEFT OUTER JOIN comments_posts ON ... INNER JOIN comments ... + # Post.find :all, :include => [:comments, :special_comments], :joins => "inner join comments ..." + # # => SELECT ... FROM posts LEFT OUTER JOIN comments comments_posts ON ... + # LEFT OUTER JOIN comments special_comments_posts ... + # INNER JOIN comments ... + # + # Table aliases are automatically truncated according to the maximum length of table identifiers according to the specific database. + # + # == Modules + # + # By default, associations will look for objects within the current module scope. Consider: + # + # module MyApplication + # module Business + # class Firm < ActiveRecord::Base + # has_many :clients + # end + # + # class Company < ActiveRecord::Base; end + # end + # end + # + # When Firm#clients is called, it'll in turn call MyApplication::Business::Company.find(firm.id). If you want to associate + # with a class in another module scope this can be done by specifying the complete class name, such as: + # + # module MyApplication + # module Business + # class Firm < ActiveRecord::Base; end + # end + # + # module Billing + # class Account < ActiveRecord::Base + # belongs_to :firm, :class_name => "MyApplication::Business::Firm" + # end + # end + # end + # + # == Type safety with ActiveRecord::AssociationTypeMismatch + # + # If you attempt to assign an object to an association that doesn't match the inferred or specified :class_name, you'll + # get a ActiveRecord::AssociationTypeMismatch. + # + # == Options + # + # All of the association macros can be specialized through options which makes more complex cases than the simple and guessable ones + # possible. + module ClassMethods + # Adds the following methods for retrieval and query of collections of associated objects. + # +collection+ is replaced with the symbol passed as the first argument, so + # has_many :clients would add among others clients.empty?. + # * collection(force_reload = false) - returns an array of all the associated objects. + # An empty array is returned if none are found. + # * collection<<(object, ...) - adds one or more objects to the collection by setting their foreign keys to the collection's primary key. + # * collection.delete(object, ...) - removes one or more objects from the collection by setting their foreign keys to NULL. + # This will also destroy the objects if they're declared as belongs_to and dependent on this model. + # * collection=objects - replaces the collections content by deleting and adding objects as appropriate. + # * collection_singular_ids=ids - replace the collection by the objects identified by the primary keys in +ids+ + # * collection.clear - removes every object from the collection. This destroys the associated objects if they + # are :dependent, deletes them directly from the database if they are :dependent => :delete_all, + # and sets their foreign keys to NULL otherwise. + # * collection.empty? - returns true if there are no associated objects. + # * collection.size - returns the number of associated objects. + # * collection.find - finds an associated object according to the same rules as Base.find. + # * collection.build(attributes = {}) - returns a new object of the collection type that has been instantiated + # with +attributes+ and linked to this object through a foreign key but has not yet been saved. *Note:* This only works if an + # associated object already exists, not if it's nil! + # * collection.create(attributes = {}) - returns a new object of the collection type that has been instantiated + # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation). + # *Note:* This only works if an associated object already exists, not if it's nil! + # + # Example: A Firm class declares has_many :clients, which will add: + # * Firm#clients (similar to Clients.find :all, :conditions => "firm_id = #{id}") + # * Firm#clients<< + # * Firm#clients.delete + # * Firm#clients= + # * Firm#client_ids= + # * Firm#clients.clear + # * Firm#clients.empty? (similar to firm.clients.size == 0) + # * Firm#clients.size (similar to Client.count "firm_id = #{id}") + # * Firm#clients.find (similar to Client.find(id, :conditions => "firm_id = #{id}")) + # * Firm#clients.build (similar to Client.new("firm_id" => id)) + # * Firm#clients.create (similar to c = Client.new("firm_id" => id); c.save; c) + # The declaration can also include an options hash to specialize the behavior of the association. + # + # Options are: + # * :class_name - specify the class name of the association. Use it only if that name can't be inferred + # from the association name. So has_many :products will by default be linked to the +Product+ class, but + # if the real class name is +SpecialProduct+, you'll have to specify it with this option. + # * :conditions - specify the conditions that the associated objects must meet in order to be included as a "WHERE" + # sql fragment, such as "price > 5 AND name LIKE 'B%'". + # * :order - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, + # such as "last_name, first_name DESC" + # * :group - specify the attribute by which the associated objects are returned as a "GROUP BY" sql fragment, + # such as "category" + # * :foreign_key - specify the foreign key used for the association. By default this is guessed to be the name + # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_many association will use "person_id" + # as the default foreign_key. + # * :dependent - if set to :destroy all the associated objects are destroyed + # alongside this object by calling their destroy method. If set to :delete_all all associated + # objects are deleted *without* calling their destroy method. If set to :nullify all associated + # objects' foreign keys are set to NULL *without* calling their save callbacks. + # NOTE: :dependent => true is deprecated and has been replaced with :dependent => :destroy. + # May not be set if :exclusively_dependent is also set. + # * :exclusively_dependent - Deprecated; equivalent to :dependent => :delete_all. If set to true all + # the associated object are deleted in one SQL statement without having their + # before_destroy callback run. This should only be used on associations that depend solely on this class and don't need to do any + # clean-up in before_destroy. The upside is that it's much faster, especially if there's a counter_cache involved. + # May not be set if :dependent is also set. + # * :finder_sql - specify a complete SQL statement to fetch the association. This is a good way to go for complex + # associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added. + # * :counter_sql - specify a complete SQL statement to fetch the size of the association. If +:finder_sql+ is + # specified but +:counter_sql+, +:counter_sql+ will be generated by replacing SELECT ... FROM with SELECT COUNT(*) FROM. + # * :extend - specify a named module for extending the proxy, see "Association extensions". + # * :include - specify second-order associations that should be eager loaded when the collection is loaded. + # * :group: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. + # * :limit: An integer determining the limit on the number of rows that should be returned. + # * :offset: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. + # * :select: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not + # include the joined columns. + # * :as: Specifies a polymorphic interface (See #belongs_to). + # * :through: Specifies a Join Model to perform the query through. Options for :class_name and :foreign_key + # are ignored, as the association uses the source reflection. You can only use a :through query through a belongs_to + # or has_many association. + # * :source: Specifies the source association name used by has_many :through queries. Only use it if the name cannot be + # inferred from the association. has_many :subscribers, :through => :subscriptions will look for either +:subscribers+ or + # +:subscriber+ on +Subscription+, unless a +:source+ is given. + # + # Option examples: + # has_many :comments, :order => "posted_on" + # has_many :comments, :include => :author + # has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name" + # has_many :tracks, :order => "position", :dependent => :destroy + # has_many :comments, :dependent => :nullify + # has_many :tags, :as => :taggable + # has_many :subscribers, :through => :subscriptions, :source => :user + # has_many :subscribers, :class_name => "Person", :finder_sql => + # 'SELECT DISTINCT people.* ' + + # 'FROM people p, post_subscriptions ps ' + + # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' + + # 'ORDER BY p.first_name' + def has_many(association_id, options = {}, &extension) + reflection = create_has_many_reflection(association_id, options, &extension) + + configure_dependency_for_has_many(reflection) + + if options[:through] + collection_reader_method(reflection, HasManyThroughAssociation) + else + add_multiple_associated_save_callbacks(reflection.name) + add_association_callbacks(reflection.name, reflection.options) + collection_accessor_methods(reflection, HasManyAssociation) + end + + add_deprecated_api_for_has_many(reflection.name) + end + + # Adds the following methods for retrieval and query of a single associated object. + # +association+ is replaced with the symbol passed as the first argument, so + # has_one :manager would add among others manager.nil?. + # * association(force_reload = false) - returns the associated object. Nil is returned if none is found. + # * association=(associate) - assigns the associate object, extracts the primary key, sets it as the foreign key, + # and saves the associate object. + # * association.nil? - returns true if there is no associated object. + # * build_association(attributes = {}) - returns a new object of the associated type that has been instantiated + # with +attributes+ and linked to this object through a foreign key but has not yet been saved. Note: This ONLY works if + # an association already exists. It will NOT work if the association is nil. + # * create_association(attributes = {}) - returns a new object of the associated type that has been instantiated + # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation). + # + # Example: An Account class declares has_one :beneficiary, which will add: + # * Account#beneficiary (similar to Beneficiary.find(:first, :conditions => "account_id = #{id}")) + # * Account#beneficiary=(beneficiary) (similar to beneficiary.account_id = account.id; beneficiary.save) + # * Account#beneficiary.nil? + # * Account#build_beneficiary (similar to Beneficiary.new("account_id" => id)) + # * Account#create_beneficiary (similar to b = Beneficiary.new("account_id" => id); b.save; b) + # + # The declaration can also include an options hash to specialize the behavior of the association. + # + # Options are: + # * :class_name - specify the class name of the association. Use it only if that name can't be inferred + # from the association name. So has_one :manager will by default be linked to the +Manager+ class, but + # if the real class name is +Person+, you'll have to specify it with this option. + # * :conditions - specify the conditions that the associated object must meet in order to be included as a "WHERE" + # sql fragment, such as "rank = 5". + # * :order - specify the order from which the associated object will be picked at the top. Specified as + # an "ORDER BY" sql fragment, such as "last_name, first_name DESC" + # * :dependent - if set to :destroy (or true) all the associated objects are destroyed when this object is. Also, + # association is assigned. + # * :foreign_key - specify the foreign key used for the association. By default this is guessed to be the name + # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_one association will use "person_id" + # as the default foreign_key. + # * :include - specify second-order associations that should be eager loaded when this object is loaded. + # + # Option examples: + # has_one :credit_card, :dependent => :destroy # destroys the associated credit card + # has_one :credit_card, :dependent => :nullify # updates the associated records foriegn key value to null rather than destroying it + # has_one :last_comment, :class_name => "Comment", :order => "posted_on" + # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'" + def has_one(association_id, options = {}) + reflection = create_has_one_reflection(association_id, options) + + module_eval do + after_save <<-EOF + association = instance_variable_get("@#{reflection.name}") + unless association.nil? + association["#{reflection.primary_key_name}"] = id + association.save(true) + end + EOF + end + + association_accessor_methods(reflection, HasOneAssociation) + association_constructor_method(:build, reflection, HasOneAssociation) + association_constructor_method(:create, reflection, HasOneAssociation) + + configure_dependency_for_has_one(reflection) + + # deprecated api + deprecated_has_association_method(reflection.name) + deprecated_association_comparison_method(reflection.name, reflection.class_name) + end + + # Adds the following methods for retrieval and query for a single associated object that this object holds an id to. + # +association+ is replaced with the symbol passed as the first argument, so + # belongs_to :author would add among others author.nil?. + # * association(force_reload = false) - returns the associated object. Nil is returned if none is found. + # * association=(associate) - assigns the associate object, extracts the primary key, and sets it as the foreign key. + # * association.nil? - returns true if there is no associated object. + # * build_association(attributes = {}) - returns a new object of the associated type that has been instantiated + # with +attributes+ and linked to this object through a foreign key but has not yet been saved. + # * create_association(attributes = {}) - returns a new object of the associated type that has been instantiated + # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation). + # + # Example: A Post class declares belongs_to :author, which will add: + # * Post#author (similar to Author.find(author_id)) + # * Post#author=(author) (similar to post.author_id = author.id) + # * Post#author? (similar to post.author == some_author) + # * Post#author.nil? + # * Post#build_author (similar to post.author = Author.new) + # * Post#create_author (similar to post.author = Author.new; post.author.save; post.author) + # The declaration can also include an options hash to specialize the behavior of the association. + # + # Options are: + # * :class_name - specify the class name of the association. Use it only if that name can't be inferred + # from the association name. So has_one :author will by default be linked to the +Author+ class, but + # if the real class name is +Person+, you'll have to specify it with this option. + # * :conditions - specify the conditions that the associated object must meet in order to be included as a "WHERE" + # sql fragment, such as "authorized = 1". + # * :order - specify the order from which the associated object will be picked at the top. Specified as + # an "ORDER BY" sql fragment, such as "last_name, first_name DESC" + # * :foreign_key - specify the foreign key used for the association. By default this is guessed to be the name + # of the associated class in lower-case and "_id" suffixed. So a +Person+ class that makes a belongs_to association to a + # +Boss+ class will use "boss_id" as the default foreign_key. + # * :counter_cache - caches the number of belonging objects on the associate class through use of increment_counter + # and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it's + # destroyed. This requires that a column named "#{table_name}_count" (such as comments_count for a belonging Comment class) + # is used on the associate class (such as a Post class). You can also specify a custom counter cache column by given that + # name instead of a true/false value to this option (e.g., :counter_cache => :my_custom_counter.) + # * :include - specify second-order associations that should be eager loaded when this object is loaded. + # * :polymorphic - specify this association is a polymorphic association by passing true. + # + # Option examples: + # belongs_to :firm, :foreign_key => "client_of" + # belongs_to :author, :class_name => "Person", :foreign_key => "author_id" + # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id", + # :conditions => 'discounts > #{payments_count}' + # belongs_to :attachable, :polymorphic => true + def belongs_to(association_id, options = {}) + reflection = create_belongs_to_reflection(association_id, options) + + if reflection.options[:polymorphic] + association_accessor_methods(reflection, BelongsToPolymorphicAssociation) + + module_eval do + before_save <<-EOF + association = instance_variable_get("@#{reflection.name}") + if !association.nil? + if association.new_record? + association.save(true) + end + + if association.updated? + self["#{reflection.primary_key_name}"] = association.id + self["#{reflection.options[:foreign_type]}"] = association.class.base_class.name.to_s + end + end + EOF + end + else + association_accessor_methods(reflection, BelongsToAssociation) + association_constructor_method(:build, reflection, BelongsToAssociation) + association_constructor_method(:create, reflection, BelongsToAssociation) + + module_eval do + before_save <<-EOF + association = instance_variable_get("@#{reflection.name}") + if !association.nil? + if association.new_record? + association.save(true) + end + + if association.updated? + self["#{reflection.primary_key_name}"] = association.id + end + end + EOF + end + + # deprecated api + deprecated_has_association_method(reflection.name) + deprecated_association_comparison_method(reflection.name, reflection.class_name) + end + + if options[:counter_cache] + cache_column = options[:counter_cache] == true ? + "#{self.to_s.underscore.pluralize}_count" : + options[:counter_cache] + + module_eval( + "after_create '#{reflection.name}.class.increment_counter(\"#{cache_column}\", #{reflection.primary_key_name})" + + " unless #{reflection.name}.nil?'" + ) + + module_eval( + "before_destroy '#{reflection.name}.class.decrement_counter(\"#{cache_column}\", #{reflection.primary_key_name})" + + " unless #{reflection.name}.nil?'" + ) + end + end + + # Associates two classes via an intermediate join table. Unless the join table is explicitly specified as + # an option, it is guessed using the lexical order of the class names. So a join between Developer and Project + # will give the default join table name of "developers_projects" because "D" outranks "P". + # + # Deprecated: Any additional fields added to the join table will be placed as attributes when pulling records out through + # has_and_belongs_to_many associations. Records returned from join tables with additional attributes will be marked as + # ReadOnly (because we can't save changes to the additional attrbutes). It's strongly recommended that you upgrade any + # associations with attributes to a real join model (see introduction). + # + # Adds the following methods for retrieval and query. + # +collection+ is replaced with the symbol passed as the first argument, so + # has_and_belongs_to_many :categories would add among others categories.empty?. + # * collection(force_reload = false) - returns an array of all the associated objects. + # An empty array is returned if none is found. + # * collection<<(object, ...) - adds one or more objects to the collection by creating associations in the join table + # (collection.push and collection.concat are aliases to this method). + # * collection.push_with_attributes(object, join_attributes) - adds one to the collection by creating an association in the join table that + # also holds the attributes from join_attributes (should be a hash with the column names as keys). This can be used to have additional + # attributes on the join, which will be injected into the associated objects when they are retrieved through the collection. + # (collection.concat_with_attributes is an alias to this method). This method is now deprecated. + # * collection.delete(object, ...) - removes one or more objects from the collection by removing their associations from the join table. + # This does not destroy the objects. + # * collection=objects - replaces the collections content by deleting and adding objects as appropriate. + # * collection_singular_ids=ids - replace the collection by the objects identified by the primary keys in +ids+ + # * collection.clear - removes every object from the collection. This does not destroy the objects. + # * collection.empty? - returns true if there are no associated objects. + # * collection.size - returns the number of associated objects. + # * collection.find(id) - finds an associated object responding to the +id+ and that + # meets the condition that it has to be associated with this object. + # + # Example: An Developer class declares has_and_belongs_to_many :projects, which will add: + # * Developer#projects + # * Developer#projects<< + # * Developer#projects.push_with_attributes + # * Developer#projects.delete + # * Developer#projects= + # * Developer#project_ids= + # * Developer#projects.clear + # * Developer#projects.empty? + # * Developer#projects.size + # * Developer#projects.find(id) + # The declaration may include an options hash to specialize the behavior of the association. + # + # Options are: + # * :class_name - specify the class name of the association. Use it only if that name can't be inferred + # from the association name. So has_and_belongs_to_many :projects will by default be linked to the + # +Project+ class, but if the real class name is +SuperProject+, you'll have to specify it with this option. + # * :join_table - specify the name of the join table if the default based on lexical order isn't what you want. + # WARNING: If you're overwriting the table name of either class, the table_name method MUST be declared underneath any + # has_and_belongs_to_many declaration in order to work. + # * :foreign_key - specify the foreign key used for the association. By default this is guessed to be the name + # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_and_belongs_to_many association + # will use "person_id" as the default foreign_key. + # * :association_foreign_key - specify the association foreign key used for the association. By default this is + # guessed to be the name of the associated class in lower-case and "_id" suffixed. So if the associated class is +Project+, + # the has_and_belongs_to_many association will use "project_id" as the default association foreign_key. + # * :conditions - specify the conditions that the associated object must meet in order to be included as a "WHERE" + # sql fragment, such as "authorized = 1". + # * :order - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, such as "last_name, first_name DESC" + # * :uniq - if set to true, duplicate associated objects will be ignored by accessors and query methods + # * :finder_sql - overwrite the default generated SQL used to fetch the association with a manual one + # * :delete_sql - overwrite the default generated SQL used to remove links between the associated + # classes with a manual one + # * :insert_sql - overwrite the default generated SQL used to add links between the associated classes + # with a manual one + # * :extend - anonymous module for extending the proxy, see "Association extensions". + # * :include - specify second-order associations that should be eager loaded when the collection is loaded. + # * :group: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. + # * :limit: An integer determining the limit on the number of rows that should be returned. + # * :offset: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. + # * :select: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not + # include the joined columns. + # + # Option examples: + # has_and_belongs_to_many :projects + # has_and_belongs_to_many :projects, :include => [ :milestones, :manager ] + # has_and_belongs_to_many :nations, :class_name => "Country" + # has_and_belongs_to_many :categories, :join_table => "prods_cats" + # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql => + # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}' + def has_and_belongs_to_many(association_id, options = {}, &extension) + reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension) + + add_multiple_associated_save_callbacks(reflection.name) + collection_accessor_methods(reflection, HasAndBelongsToManyAssociation) + + # Don't use a before_destroy callback since users' before_destroy + # callbacks will be executed after the association is wiped out. + old_method = "destroy_without_habtm_shim_for_#{reflection.name}" + class_eval <<-end_eval + alias_method :#{old_method}, :destroy_without_callbacks + def destroy_without_callbacks + #{reflection.name}.clear + #{old_method} + end + end_eval + + add_association_callbacks(reflection.name, options) + + # deprecated api + deprecated_collection_count_method(reflection.name) + deprecated_add_association_relation(reflection.name) + deprecated_remove_association_relation(reflection.name) + deprecated_has_collection_method(reflection.name) + end + + private + def join_table_name(first_table_name, second_table_name) + if first_table_name < second_table_name + join_table = "#{first_table_name}_#{second_table_name}" + else + join_table = "#{second_table_name}_#{first_table_name}" + end + + table_name_prefix + join_table + table_name_suffix + end + + def association_accessor_methods(reflection, association_proxy_class) + define_method(reflection.name) do |*params| + force_reload = params.first unless params.empty? + association = instance_variable_get("@#{reflection.name}") + + if association.nil? || force_reload + association = association_proxy_class.new(self, reflection) + retval = association.reload + unless retval.nil? + instance_variable_set("@#{reflection.name}", association) + else + instance_variable_set("@#{reflection.name}", nil) + return nil + end + end + association + end + + define_method("#{reflection.name}=") do |new_value| + association = instance_variable_get("@#{reflection.name}") + if association.nil? + association = association_proxy_class.new(self, reflection) + end + + association.replace(new_value) + + unless new_value.nil? + instance_variable_set("@#{reflection.name}", association) + else + instance_variable_set("@#{reflection.name}", nil) + return nil + end + + association + end + + define_method("set_#{reflection.name}_target") do |target| + return if target.nil? + association = association_proxy_class.new(self, reflection) + association.target = target + instance_variable_set("@#{reflection.name}", association) + end + end + + def collection_reader_method(reflection, association_proxy_class) + define_method(reflection.name) do |*params| + force_reload = params.first unless params.empty? + association = instance_variable_get("@#{reflection.name}") + + unless association.respond_to?(:loaded?) + association = association_proxy_class.new(self, reflection) + instance_variable_set("@#{reflection.name}", association) + end + + association.reload if force_reload + + association + end + end + + def collection_accessor_methods(reflection, association_proxy_class) + collection_reader_method(reflection, association_proxy_class) + + define_method("#{reflection.name}=") do |new_value| + association = instance_variable_get("@#{reflection.name}") + unless association.respond_to?(:loaded?) + association = association_proxy_class.new(self, reflection) + instance_variable_set("@#{reflection.name}", association) + end + association.replace(new_value) + association + end + + define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value| + send("#{reflection.name}=", reflection.class_name.constantize.find(new_value)) + end + end + + def require_association_class(class_name) + require_association(Inflector.underscore(class_name)) if class_name + end + + def add_multiple_associated_save_callbacks(association_name) + method_name = "validate_associated_records_for_#{association_name}".to_sym + define_method(method_name) do + association = instance_variable_get("@#{association_name}") + if association.respond_to?(:loaded?) + if new_record? + association + else + association.select { |record| record.new_record? } + end.each do |record| + errors.add "#{association_name}" unless record.valid? + end + end + end + + validate method_name + before_save("@new_record_before_save = new_record?; true") + + after_callback = <<-end_eval + association = instance_variable_get("@#{association_name}") + + if association.respond_to?(:loaded?) + if @new_record_before_save + records_to_save = association + else + records_to_save = association.select { |record| record.new_record? } + end + 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 + end + end_eval + + # Doesn't use after_save as that would save associations added in after_create/after_update twice + after_create(after_callback) + after_update(after_callback) + end + + def association_constructor_method(constructor, reflection, association_proxy_class) + define_method("#{constructor}_#{reflection.name}") do |*params| + attributees = params.first unless params.empty? + replace_existing = params[1].nil? ? true : params[1] + association = instance_variable_get("@#{reflection.name}") + + if association.nil? + association = association_proxy_class.new(self, reflection) + instance_variable_set("@#{reflection.name}", association) + end + + if association_proxy_class == HasOneAssociation + association.send(constructor, attributees, replace_existing) + else + association.send(constructor, attributees) + end + end + end + + def count_with_associations(options = {}) + catch :invalid_query do + join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins]) + return count_by_sql(construct_counter_sql_with_included_associations(options, join_dependency)) + end + 0 + end + + def find_with_associations(options = {}) + catch :invalid_query do + join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins]) + rows = select_all_rows(options, join_dependency) + return join_dependency.instantiate(rows) + end + [] + end + + def configure_dependency_for_has_many(reflection) + if reflection.options[:dependent] && reflection.options[:exclusively_dependent] + raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.' + end + + if reflection.options[:exclusively_dependent] + reflection.options[:dependent] = :delete_all + #warn "The :exclusively_dependent option is deprecated. Please use :dependent => :delete_all instead.") + end + + # See HasManyAssociation#delete_records. Dependent associations + # delete children, otherwise foreign key is set to NULL. + + # Add polymorphic type if the :as option is present + dependent_conditions = %(#{reflection.primary_key_name} = \#{record.quoted_id}) + if reflection.options[:as] + dependent_conditions += " AND #{reflection.options[:as]}_type = '#{base_class.name}'" + end + + case reflection.options[:dependent] + when :destroy, true + module_eval "before_destroy '#{reflection.name}.each { |o| o.destroy }'" + when :delete_all + module_eval "before_destroy { |record| #{reflection.class_name}.delete_all(%(#{dependent_conditions})) }" + when :nullify + module_eval "before_destroy { |record| #{reflection.class_name}.update_all(%(#{reflection.primary_key_name} = NULL), %(#{dependent_conditions})) }" + when nil, false + # pass + else + raise ArgumentError, 'The :dependent option expects either :destroy, :delete_all, or :nullify' + end + end + + def configure_dependency_for_has_one(reflection) + case reflection.options[:dependent] + when :destroy, true + module_eval "before_destroy '#{reflection.name}.destroy unless #{reflection.name}.nil?'" + when :nullify + module_eval "before_destroy '#{reflection.name}.update_attribute(\"#{reflection.primary_key_name}\", nil)'" + when nil, false + # pass + else + raise ArgumentError, "The :dependent option expects either :destroy or :nullify." + end + end + + + def add_deprecated_api_for_has_many(association_name) + deprecated_collection_count_method(association_name) + deprecated_add_association_relation(association_name) + deprecated_remove_association_relation(association_name) + deprecated_has_collection_method(association_name) + deprecated_find_in_collection_method(association_name) + deprecated_find_all_in_collection_method(association_name) + deprecated_collection_create_method(association_name) + deprecated_collection_build_method(association_name) + end + + def create_has_many_reflection(association_id, options, &extension) + options.assert_valid_keys( + :class_name, :table_name, :foreign_key, + :exclusively_dependent, :dependent, + :select, :conditions, :include, :order, :group, :limit, :offset, + :as, :through, :source, + :finder_sql, :counter_sql, + :before_add, :after_add, :before_remove, :after_remove, + :extend + ) + + options[:extend] = create_extension_module(association_id, extension) if block_given? + + create_reflection(:has_many, association_id, options, self) + end + + def create_has_one_reflection(association_id, options) + options.assert_valid_keys( + :class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as + ) + + create_reflection(:has_one, association_id, options, self) + end + + def create_belongs_to_reflection(association_id, options) + options.assert_valid_keys( + :class_name, :foreign_key, :foreign_type, :remote, :conditions, :order, :include, :dependent, + :counter_cache, :extend, :polymorphic + ) + + reflection = create_reflection(:belongs_to, association_id, options, self) + + if options[:polymorphic] + reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type" + end + + reflection + end + + def create_has_and_belongs_to_many_reflection(association_id, options, &extension) + options.assert_valid_keys( + :class_name, :table_name, :join_table, :foreign_key, :association_foreign_key, + :select, :conditions, :include, :order, :group, :limit, :offset, + :finder_sql, :delete_sql, :insert_sql, :uniq, + :before_add, :after_add, :before_remove, :after_remove, + :extend + ) + + options[:extend] = create_extension_module(association_id, extension) if block_given? + + reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self) + + reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name)) + + reflection + end + + def reflect_on_included_associations(associations) + [ associations ].flatten.collect { |association| reflect_on_association(association.to_s.intern) } + end + + def guard_against_unlimitable_reflections(reflections, options) + if (options[:offset] || options[:limit]) && !using_limitable_reflections?(reflections) + raise( + ConfigurationError, + "You can not use offset and limit together with has_many or has_and_belongs_to_many associations" + ) + end + end + + def select_all_rows(options, join_dependency) + connection.select_all( + construct_finder_sql_with_included_associations(options, join_dependency), + "#{name} Load Including Associations" + ) + end + + def construct_counter_sql_with_included_associations(options, join_dependency) + scope = scope(:find) + sql = "SELECT COUNT(DISTINCT #{table_name}.#{primary_key})" + + # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT. + if !Base.connection.supports_count_distinct? + sql = "SELECT COUNT(*) FROM (SELECT DISTINCT #{table_name}.#{primary_key}" + end + + sql << " FROM #{table_name} " + sql << join_dependency.join_associations.collect{|join| join.association_join }.join + + add_joins!(sql, options, scope) + add_conditions!(sql, options[:conditions], scope) + add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit]) + + add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections) + + if !Base.connection.supports_count_distinct? + sql << ")" + end + + return sanitize_sql(sql) + end + + def construct_finder_sql_with_included_associations(options, join_dependency) + scope = scope(:find) + sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || table_name} " + sql << join_dependency.join_associations.collect{|join| join.association_join }.join + + add_joins!(sql, options, scope) + add_conditions!(sql, options[:conditions], scope) + add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && options[:limit] + + sql << "ORDER BY #{options[:order]} " if options[:order] + + add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections) + + return sanitize_sql(sql) + end + + def add_limited_ids_condition!(sql, options, join_dependency) + unless (id_list = select_limited_ids_list(options, join_dependency)).empty? + sql << "#{condition_word(sql)} #{table_name}.#{primary_key} IN (#{id_list}) " + else + throw :invalid_query + end + end + + def select_limited_ids_list(options, join_dependency) + connection.select_all( + construct_finder_sql_for_association_limiting(options, join_dependency), + "#{name} Load IDs For Limited Eager Loading" + ).collect { |row| connection.quote(row[primary_key]) }.join(", ") + end + + def construct_finder_sql_for_association_limiting(options, join_dependency) + scope = scope(:find) + sql = "SELECT " + sql << "DISTINCT #{table_name}." if include_eager_conditions?(options) || include_eager_order?(options) + sql << primary_key + sql << ", #{options[:order].split(',').collect { |s| s.split.first } * ', '}" if options[:order] && (include_eager_conditions?(options) || include_eager_order?(options)) + sql << " FROM #{table_name} " + + if include_eager_conditions?(options) || include_eager_order?(options) + sql << join_dependency.join_associations.collect{|join| join.association_join }.join + add_joins!(sql, options, scope) + end + + add_conditions!(sql, options[:conditions], scope) + sql << "ORDER BY #{options[:order]} " if options[:order] + add_limit!(sql, options, scope) + return sanitize_sql(sql) + end + + # Checks if the conditions reference a table other than the current model table + def include_eager_conditions?(options) + # look in both sets of conditions + conditions = [scope(:find, :conditions), options[:conditions]].inject([]) do |all, cond| + case cond + when nil then all + when Array then all << cond.first + else all << cond + end + end + return false unless conditions.any? + conditions.join(' ').scan(/(\w+)\.\w+/).flatten.any? do |condition_table_name| + condition_table_name != table_name + end + end + + # Checks if the query order references a table other than the current model's table. + def include_eager_order?(options) + order = options[:order] + return false unless order + order.scan(/(\w+)\.\w+/).flatten.any? do |order_table_name| + order_table_name != table_name + end + end + + def using_limitable_reflections?(reflections) + reflections.reject { |r| [ :belongs_to, :has_one ].include?(r.macro) }.length.zero? + end + + def column_aliases(join_dependency) + join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name| + "#{join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ") + end + + def add_association_callbacks(association_name, options) + callbacks = %w(before_add after_add before_remove after_remove) + callbacks.each do |callback_name| + full_callback_name = "#{callback_name.to_s}_for_#{association_name.to_s}" + defined_callbacks = options[callback_name.to_sym] + if options.has_key?(callback_name.to_sym) + class_inheritable_reader full_callback_name.to_sym + write_inheritable_array(full_callback_name.to_sym, [defined_callbacks].flatten) + end + end + end + + def condition_word(sql) + sql =~ /where/i ? " AND " : "WHERE " + end + + def create_extension_module(association_id, extension) + extension_module_name = "#{self.to_s}#{association_id.to_s.camelize}AssociationExtension" + + silence_warnings do + Object.const_set(extension_module_name, Module.new(&extension)) + end + + extension_module_name.constantize + end + + class JoinDependency + attr_reader :joins, :reflections, :table_aliases + + def initialize(base, associations, joins) + @joins = [JoinBase.new(base, joins)] + @associations = associations + @reflections = [] + @base_records_hash = {} + @base_records_in_order = [] + @table_aliases = Hash.new { |aliases, table| aliases[table] = 0 } + @table_aliases[base.table_name] = 1 + build(associations) + end + + def join_associations + @joins[1..-1].to_a + end + + def join_base + @joins[0] + end + + def instantiate(rows) + rows.each_with_index do |row, i| + primary_id = join_base.record_id(row) + unless @base_records_hash[primary_id] + @base_records_in_order << (@base_records_hash[primary_id] = join_base.instantiate(row)) + end + construct(@base_records_hash[primary_id], @associations, join_associations.dup, row) + end + return @base_records_in_order + end + + def aliased_table_names_for(table_name) + joins.select{|join| join.table_name == table_name }.collect{|join| join.aliased_table_name} + end + + protected + def build(associations, parent = nil) + parent ||= @joins.last + case associations + when Symbol, String + reflection = parent.reflections[associations.to_s.intern] or + raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" + @reflections << reflection + @joins << JoinAssociation.new(reflection, self, parent) + when Array + associations.each do |association| + build(association, parent) + end + when Hash + associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| + build(name, parent) + build(associations[name]) + end + else + raise ConfigurationError, associations.inspect + end + end + + def construct(parent, associations, joins, row) + case associations + when Symbol, String + while (join = joins.shift).reflection.name.to_s != associations.to_s + raise ConfigurationError, "Not Enough Associations" if joins.empty? + end + construct_association(parent, join, row) + when Array + associations.each do |association| + construct(parent, association, joins, row) + end + when Hash + associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| + association = construct_association(parent, joins.shift, row) + construct(association, associations[name], joins, row) if association + end + else + raise ConfigurationError, associations.inspect + end + end + + def construct_association(record, join, row) + case join.reflection.macro + when :has_many, :has_and_belongs_to_many + collection = record.send(join.reflection.name) + collection.loaded + + return nil if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil? + association = join.instantiate(row) + collection.target.push(association) unless collection.target.include?(association) + when :has_one, :belongs_to + return if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil? + association = join.instantiate(row) + record.send("set_#{join.reflection.name}_target", association) + else + raise ConfigurationError, "unknown macro: #{join.reflection.macro}" + end + return association + end + + class JoinBase + attr_reader :active_record, :table_joins + delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :to => :active_record + + def initialize(active_record, joins = nil) + @active_record = active_record + @cached_record = {} + @table_joins = joins + end + + def aliased_prefix + "t0" + end + + def aliased_primary_key + "#{ aliased_prefix }_r0" + end + + def aliased_table_name + active_record.table_name + end + + def column_names_with_alias + unless @column_names_with_alias + @column_names_with_alias = [] + ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i| + @column_names_with_alias << [column_name, "#{ aliased_prefix }_r#{ i }"] + end + end + return @column_names_with_alias + end + + def extract_record(row) + column_names_with_alias.inject({}){|record, (cn, an)| record[cn] = row[an]; record} + end + + def record_id(row) + row[aliased_primary_key] + end + + def instantiate(row) + @cached_record[record_id(row)] ||= active_record.instantiate(extract_record(row)) + end + end + + class JoinAssociation < JoinBase + attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix, :aliased_join_table_name, :parent_table_name + delegate :options, :klass, :through_reflection, :source_reflection, :to => :reflection + + def initialize(reflection, join_dependency, parent = nil) + reflection.check_validity! + if reflection.options[:polymorphic] + raise EagerLoadPolymorphicError.new(reflection) + end + + super(reflection.klass) + @parent = parent + @reflection = reflection + @aliased_prefix = "t#{ join_dependency.joins.size }" + @aliased_table_name = table_name # start with the table name + @parent_table_name = parent.active_record.table_name + + if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{aliased_table_name.downcase}\son} + join_dependency.table_aliases[aliased_table_name] += 1 + end + + unless join_dependency.table_aliases[aliased_table_name].zero? + # if the table name has been used, then use an alias + @aliased_table_name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}" + table_index = join_dependency.table_aliases[aliased_table_name] + @aliased_table_name = @aliased_table_name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0 + end + join_dependency.table_aliases[aliased_table_name] += 1 + + if reflection.macro == :has_and_belongs_to_many || (reflection.macro == :has_many && reflection.options[:through]) + @aliased_join_table_name = reflection.macro == :has_and_belongs_to_many ? reflection.options[:join_table] : reflection.through_reflection.klass.table_name + unless join_dependency.table_aliases[aliased_join_table_name].zero? + @aliased_join_table_name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}_join" + table_index = join_dependency.table_aliases[aliased_join_table_name] + @aliased_join_table_name = @aliased_join_table_name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0 + end + join_dependency.table_aliases[aliased_join_table_name] += 1 + end + end + + def association_join + join = case reflection.macro + when :has_and_belongs_to_many + " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [ + table_alias_for(options[:join_table], aliased_join_table_name), + aliased_join_table_name, + options[:foreign_key] || reflection.active_record.to_s.classify.foreign_key, + reflection.active_record.table_name, reflection.active_record.primary_key] + + " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [ + table_name_and_alias, aliased_table_name, klass.primary_key, + aliased_join_table_name, options[:association_foreign_key] || klass.table_name.classify.foreign_key + ] + when :has_many, :has_one + case + when reflection.macro == :has_many && reflection.options[:through] + through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : '' + if through_reflection.options[:as] # has_many :through against a polymorphic join + polymorphic_foreign_key = through_reflection.options[:as].to_s + '_id' + polymorphic_foreign_type = through_reflection.options[:as].to_s + '_type' + + " LEFT OUTER JOIN %s ON (%s.%s = %s.%s AND %s.%s = %s) " % [ + table_alias_for(through_reflection.klass.table_name, aliased_join_table_name), + aliased_join_table_name, polymorphic_foreign_key, + parent.aliased_table_name, parent.primary_key, + aliased_join_table_name, polymorphic_foreign_type, klass.quote(parent.active_record.base_class.name)] + + " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [table_name_and_alias, + aliased_table_name, primary_key, aliased_join_table_name, options[:foreign_key] || reflection.klass.to_s.classify.foreign_key + ] + else + if source_reflection.macro == :has_many && source_reflection.options[:as] + " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [ + table_alias_for(through_reflection.klass.table_name, aliased_join_table_name), aliased_join_table_name, + through_reflection.primary_key_name, + parent.aliased_table_name, parent.primary_key] + + " LEFT OUTER JOIN %s ON %s.%s = %s.%s AND %s.%s = %s " % [ + table_name_and_alias, + aliased_table_name, "#{source_reflection.options[:as]}_id", + aliased_join_table_name, options[:foreign_key] || primary_key, + aliased_table_name, "#{source_reflection.options[:as]}_type", + klass.quote(source_reflection.active_record.base_class.name) + ] + else + case source_reflection.macro + when :belongs_to + first_key = primary_key + second_key = options[:foreign_key] || klass.to_s.classify.foreign_key + when :has_many + first_key = through_reflection.klass.to_s.classify.foreign_key + second_key = options[:foreign_key] || primary_key + end + + " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [ + table_alias_for(through_reflection.klass.table_name, aliased_join_table_name), aliased_join_table_name, + through_reflection.primary_key_name, + parent.aliased_table_name, parent.primary_key] + + " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [ + table_name_and_alias, + aliased_table_name, first_key, + aliased_join_table_name, second_key + ] + end + end + + when reflection.macro == :has_many && reflection.options[:as] + " LEFT OUTER JOIN %s ON %s.%s = %s.%s AND %s.%s = %s" % [ + table_name_and_alias, + aliased_table_name, "#{reflection.options[:as]}_id", + parent.aliased_table_name, parent.primary_key, + aliased_table_name, "#{reflection.options[:as]}_type", + klass.quote(parent.active_record.base_class.name) + ] + when reflection.macro == :has_one && reflection.options[:as] + " LEFT OUTER JOIN %s ON %s.%s = %s.%s AND %s.%s = %s " % [ + table_name_and_alias, + aliased_table_name, "#{reflection.options[:as]}_id", + parent.aliased_table_name, parent.primary_key, + aliased_table_name, "#{reflection.options[:as]}_type", + klass.quote(reflection.active_record.base_class.name) + ] + else + foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key + " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [ + table_name_and_alias, + aliased_table_name, foreign_key, + parent.aliased_table_name, parent.primary_key + ] + end + when :belongs_to + " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [ + table_name_and_alias, aliased_table_name, reflection.klass.primary_key, + parent.aliased_table_name, options[:foreign_key] || klass.to_s.foreign_key + ] + else + "" + end || '' + join << %(AND %s.%s = %s ) % [ + aliased_table_name, + reflection.active_record.connection.quote_column_name(reflection.active_record.inheritance_column), + klass.quote(klass.name)] unless klass.descends_from_active_record? + join << "AND #{interpolate_sql(sanitize_sql(reflection.options[:conditions]))} " if reflection.options[:conditions] + join + end + + protected + def pluralize(table_name) + ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name + end + + def table_alias_for(table_name, table_alias) + "#{table_name} #{table_alias if table_name != table_alias}".strip + end + + def table_name_and_alias + table_alias_for table_name, @aliased_table_name + end + + def interpolate_sql(sql) + instance_eval("%@#{sql.gsub('@', '\@')}@") + end + end + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/associations/association_collection.rb b/vendor/rails/activerecord/lib/active_record/associations/association_collection.rb new file mode 100644 index 00000000..268452f4 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/associations/association_collection.rb @@ -0,0 +1,160 @@ +require 'set' + +module ActiveRecord + module Associations + class AssociationCollection < AssociationProxy #:nodoc: + def to_ary + load_target + @target.to_ary + end + + def reset + @target = [] + @loaded = false + end + + # Add +records+ to this association. Returns +self+ so method calls may be chained. + # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically. + def <<(*records) + result = true + load_target + + @owner.transaction do + flatten_deeper(records).each do |record| + raise_on_type_mismatch(record) + callback(:before_add, record) + result &&= insert_record(record) unless @owner.new_record? + @target << record + callback(:after_add, record) + end + end + + result && self + end + + alias_method :push, :<< + alias_method :concat, :<< + + # Remove all records from this association + def delete_all + load_target + delete(@target) + @target = [] + end + + # Remove +records+ from this association. Does not destroy +records+. + def delete(*records) + records = flatten_deeper(records) + records.each { |record| raise_on_type_mismatch(record) } + records.reject! { |record| @target.delete(record) if record.new_record? } + return if records.empty? + + @owner.transaction do + records.each { |record| callback(:before_remove, record) } + delete_records(records) + records.each do |record| + @target.delete(record) + callback(:after_remove, record) + end + end + end + + # Removes all records from this association. Returns +self+ so method calls may be chained. + def clear + return self if length.zero? # forces load_target if hasn't happened already + + if @reflection.options[:dependent] && @reflection.options[:dependent] == :delete_all + destroy_all + else + delete_all + end + + self + end + + def destroy_all + @owner.transaction do + each { |record| record.destroy } + end + + @target = [] + end + + def create(attributes = {}) + # Can't use Base.create since the foreign key may be a protected attribute. + if attributes.is_a?(Array) + attributes.collect { |attr| create(attr) } + else + record = build(attributes) + record.save unless @owner.new_record? + record + end + end + + # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and + # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero + # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length. + def size + if loaded? then @target.size else count_records end + end + + # Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check + # whether the collection is empty, use collection.length.zero? instead of collection.empty? + def length + load_target.size + end + + def empty? + size.zero? + end + + def uniq(collection = self) + collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records } + end + + # Replace this collection with +other_array+ + # This will perform a diff and delete/add only records that have changed. + def replace(other_array) + other_array.each { |val| raise_on_type_mismatch(val) } + + load_target + other = other_array.size < 100 ? other_array : other_array.to_set + current = @target.size < 100 ? @target : @target.to_set + + @owner.transaction do + delete(@target.select { |v| !other.include?(v) }) + concat(other_array.select { |v| !current.include?(v) }) + end + end + + private + # Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems. + def flatten_deeper(array) + array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten + end + + def callback(method, record) + callbacks_for(method).each do |callback| + case callback + when Symbol + @owner.send(callback, record) + when Proc, Method + callback.call(@owner, record) + else + if callback.respond_to?(method) + callback.send(method, @owner, record) + else + raise ActiveRecordError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method." + end + end + end + end + + def callbacks_for(callback_name) + full_callback_name = "#{callback_name}_for_#{@reflection.name}" + @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || [] + end + + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/associations/association_proxy.rb b/vendor/rails/activerecord/lib/active_record/associations/association_proxy.rb new file mode 100644 index 00000000..403c036d --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/associations/association_proxy.rb @@ -0,0 +1,139 @@ +module ActiveRecord + module Associations + class AssociationProxy #:nodoc: + attr_reader :reflection + alias_method :proxy_respond_to?, :respond_to? + alias_method :proxy_extend, :extend + instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?|^proxy_extend|^send)/ } + + def initialize(owner, reflection) + @owner, @reflection = owner, reflection + proxy_extend(reflection.options[:extend]) if reflection.options[:extend] + reset + end + + def respond_to?(symbol, include_priv = false) + proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv)) + end + + # Explicitly proxy === because the instance method removal above + # doesn't catch it. + def ===(other) + load_target + other === @target + end + + def aliased_table_name + @reflection.klass.table_name + end + + def conditions + @conditions ||= eval("%(#{@reflection.active_record.send :sanitize_sql, @reflection.options[:conditions]})") if @reflection.options[:conditions] + end + alias :sql_conditions :conditions + + def reset + @target = nil + @loaded = false + end + + def reload + reset + load_target + end + + def loaded? + @loaded + end + + def loaded + @loaded = true + end + + def target + @target + end + + def target=(target) + @target = target + loaded + end + + protected + def dependent? + @reflection.options[:dependent] || false + end + + def quoted_record_ids(records) + records.map { |record| record.quoted_id }.join(',') + end + + def interpolate_sql_options!(options, *keys) + keys.each { |key| options[key] &&= interpolate_sql(options[key]) } + end + + def interpolate_sql(sql, record = nil) + @owner.send(:interpolate_sql, sql, record) + end + + def sanitize_sql(sql) + @reflection.klass.send(:sanitize_sql, sql) + end + + def extract_options_from_args!(args) + @owner.send(:extract_options_from_args!, args) + end + + def set_belongs_to_association_for(record) + if @reflection.options[:as] + record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record? + record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s + else + record[@reflection.primary_key_name] = @owner.id unless @owner.new_record? + end + end + + def merge_options_from_reflection!(options) + options.reverse_merge!( + :group => @reflection.options[:group], + :limit => @reflection.options[:limit], + :offset => @reflection.options[:offset], + :joins => @reflection.options[:joins], + :include => @reflection.options[:include], + :select => @reflection.options[:select] + ) + end + + private + def method_missing(method, *args, &block) + load_target + @target.send(method, *args, &block) + end + + def load_target + if !@owner.new_record? || foreign_key_present + begin + @target = find_target if !loaded? + rescue ActiveRecord::RecordNotFound + reset + end + end + + loaded if target + target + end + + # Can be overwritten by associations that might have the foreign key available for an association without + # having the object itself (and still being a new record). Currently, only belongs_to present this scenario. + def foreign_key_present + false + end + + def raise_on_type_mismatch(record) + unless record.is_a?(@reflection.klass) + raise ActiveRecord::AssociationTypeMismatch, "#{@reflection.class_name} expected, got #{record.class}" + end + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/associations/belongs_to_association.rb b/vendor/rails/activerecord/lib/active_record/associations/belongs_to_association.rb new file mode 100644 index 00000000..1752678c --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -0,0 +1,56 @@ +module ActiveRecord + module Associations + class BelongsToAssociation < AssociationProxy #:nodoc: + def create(attributes = {}) + replace(@reflection.klass.create(attributes)) + end + + def build(attributes = {}) + replace(@reflection.klass.new(attributes)) + end + + def replace(record) + counter_cache_name = @reflection.counter_cache_column + + if record.nil? + if counter_cache_name && @owner[counter_cache_name] && !@owner.new_record? + @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name] + end + + @target = @owner[@reflection.primary_key_name] = nil + else + raise_on_type_mismatch(record) + + if counter_cache_name && !@owner.new_record? + @reflection.klass.increment_counter(counter_cache_name, record.id) + @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name] + end + + @target = (AssociationProxy === record ? record.target : record) + @owner[@reflection.primary_key_name] = record.id unless record.new_record? + @updated = true + end + + loaded + record + end + + def updated? + @updated + end + + private + def find_target + @reflection.klass.find( + @owner[@reflection.primary_key_name], + :conditions => conditions, + :include => @reflection.options[:include] + ) + end + + def foreign_key_present + !@owner[@reflection.primary_key_name].nil? + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/vendor/rails/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb new file mode 100644 index 00000000..9549b959 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -0,0 +1,50 @@ +module ActiveRecord + module Associations + class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc: + def replace(record) + if record.nil? + @target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil + else + @target = (AssociationProxy === record ? record.target : record) + + unless record.new_record? + @owner[@reflection.primary_key_name] = record.id + @owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s + end + + @updated = true + end + + loaded + record + end + + def updated? + @updated + end + + private + def find_target + return nil if association_class.nil? + + if @reflection.options[:conditions] + association_class.find( + @owner[@reflection.primary_key_name], + :conditions => conditions, + :include => @reflection.options[:include] + ) + else + association_class.find(@owner[@reflection.primary_key_name], :include => @reflection.options[:include]) + end + end + + def foreign_key_present + !@owner[@reflection.primary_key_name].nil? + end + + def association_class + @owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/vendor/rails/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb new file mode 100644 index 00000000..cd866d2c --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -0,0 +1,169 @@ +module ActiveRecord + module Associations + class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc: + def initialize(owner, reflection) + super + construct_sql + end + + def build(attributes = {}) + load_target + record = @reflection.klass.new(attributes) + @target << record + record + end + + def find_first + load_target.first + end + + def find(*args) + options = Base.send(:extract_options_from_args!, args) + + # If using a custom finder_sql, scan the entire collection. + if @reflection.options[:finder_sql] + expects_array = args.first.kind_of?(Array) + ids = args.flatten.compact.uniq + + if ids.size == 1 + id = ids.first.to_i + record = load_target.detect { |record| id == record.id } + expects_array ? [record] : record + else + load_target.select { |record| ids.include?(record.id) } + end + else + conditions = "#{@finder_sql}" + + if sanitized_conditions = sanitize_sql(options[:conditions]) + conditions << " AND (#{sanitized_conditions})" + end + + options[:conditions] = conditions + options[:joins] = @join_sql + options[:readonly] = finding_with_ambigious_select?(options[:select]) + + if options[:order] && @reflection.options[:order] + options[:order] = "#{options[:order]}, #{@reflection.options[:order]}" + elsif @reflection.options[:order] + options[:order] = @reflection.options[:order] + end + + merge_options_from_reflection!(options) + + # Pass through args exactly as we received them. + args << options + @reflection.klass.find(*args) + end + end + + def push_with_attributes(record, join_attributes = {}) + raise_on_type_mismatch(record) + join_attributes.each { |key, value| record[key.to_s] = value } + + callback(:before_add, record) + insert_record(record) unless @owner.new_record? + @target << record + callback(:after_add, record) + + self + end + + alias :concat_with_attributes :push_with_attributes + + def size + @reflection.options[:uniq] ? count_records : super + end + + protected + def method_missing(method, *args, &block) + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) + super + else + @reflection.klass.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do + @reflection.klass.send(method, *args, &block) + end + end + end + + def find_target + if @reflection.options[:finder_sql] + records = @reflection.klass.find_by_sql(@finder_sql) + else + records = find(:all) + end + + @reflection.options[:uniq] ? uniq(records) : records + end + + def count_records + load_target.size + end + + def insert_record(record) + if record.new_record? + return false unless record.save + end + + if @reflection.options[:insert_sql] + @owner.connection.execute(interpolate_sql(@reflection.options[:insert_sql], record)) + else + columns = @owner.connection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns") + + attributes = columns.inject({}) do |attributes, column| + case column.name + when @reflection.primary_key_name + attributes[column.name] = @owner.quoted_id + when @reflection.association_foreign_key + attributes[column.name] = record.quoted_id + else + if record.attributes.has_key?(column.name) + value = @owner.send(:quote, record[column.name], column) + attributes[column.name] = value unless value.nil? + end + end + attributes + end + + sql = + "INSERT INTO #{@reflection.options[:join_table]} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " + + "VALUES (#{attributes.values.join(', ')})" + + @owner.connection.execute(sql) + end + + return true + end + + def delete_records(records) + if sql = @reflection.options[:delete_sql] + records.each { |record| @owner.connection.execute(interpolate_sql(sql, record)) } + else + ids = quoted_record_ids(records) + sql = "DELETE FROM #{@reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})" + @owner.connection.execute(sql) + end + end + + def construct_sql + interpolate_sql_options!(@reflection.options, :finder_sql) + + if @reflection.options[:finder_sql] + @finder_sql = @reflection.options[:finder_sql] + else + @finder_sql = "#{@reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} " + @finder_sql << " AND (#{conditions})" if conditions + end + + @join_sql = "INNER JOIN #{@reflection.options[:join_table]} ON #{@reflection.klass.table_name}.#{@reflection.klass.primary_key} = #{@reflection.options[:join_table]}.#{@reflection.association_foreign_key}" + end + + # Join tables with additional columns on top of the two foreign keys must be considered ambigious unless a select + # clause has been explicitly defined. Otherwise you can get broken records back, if, say, the join column also has + # and id column, which will then overwrite the id column of the records coming back. + def finding_with_ambigious_select?(select_clause) + !select_clause && @owner.connection.columns(@reflection.options[:join_table], "Join Table Columns").size != 2 + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/associations/has_many_association.rb b/vendor/rails/activerecord/lib/active_record/associations/has_many_association.rb new file mode 100644 index 00000000..52cad794 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/associations/has_many_association.rb @@ -0,0 +1,190 @@ +module ActiveRecord + module Associations + class HasManyAssociation < AssociationCollection #:nodoc: + def initialize(owner, reflection) + super + construct_sql + end + + def build(attributes = {}) + if attributes.is_a?(Array) + attributes.collect { |attr| build(attr) } + else + load_target + record = @reflection.klass.new(attributes) + set_belongs_to_association_for(record) + @target << record + record + end + end + + # DEPRECATED. + def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil) + if @reflection.options[:finder_sql] + @reflection.klass.find_by_sql(@finder_sql) + else + conditions = @finder_sql + conditions += " AND (#{sanitize_sql(runtime_conditions)})" if runtime_conditions + orderings ||= @reflection.options[:order] + @reflection.klass.find_all(conditions, orderings, limit, joins) + end + end + + # DEPRECATED. Find the first associated record. All arguments are optional. + def find_first(conditions = nil, orderings = nil) + find_all(conditions, orderings, 1).first + end + + # Count the number of associated records. All arguments are optional. + def count(runtime_conditions = nil) + if @reflection.options[:counter_sql] + @reflection.klass.count_by_sql(@counter_sql) + elsif @reflection.options[:finder_sql] + @reflection.klass.count_by_sql(@finder_sql) + else + sql = @finder_sql + sql += " AND (#{sanitize_sql(runtime_conditions)})" if runtime_conditions + @reflection.klass.count(sql) + end + end + + def find(*args) + options = Base.send(:extract_options_from_args!, args) + + # If using a custom finder_sql, scan the entire collection. + if @reflection.options[:finder_sql] + expects_array = args.first.kind_of?(Array) + ids = args.flatten.compact.uniq + + if ids.size == 1 + id = ids.first + record = load_target.detect { |record| id == record.id } + expects_array ? [ record ] : record + else + load_target.select { |record| ids.include?(record.id) } + end + else + conditions = "#{@finder_sql}" + if sanitized_conditions = sanitize_sql(options[:conditions]) + conditions << " AND (#{sanitized_conditions})" + end + options[:conditions] = conditions + + if options[:order] && @reflection.options[:order] + options[:order] = "#{options[:order]}, #{@reflection.options[:order]}" + elsif @reflection.options[:order] + options[:order] = @reflection.options[:order] + end + + merge_options_from_reflection!(options) + + # Pass through args exactly as we received them. + args << options + @reflection.klass.find(*args) + end + end + + protected + def method_missing(method, *args, &block) + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) + super + else + @reflection.klass.with_scope( + :find => { + :conditions => @finder_sql, + :joins => @join_sql, + :readonly => false + }, + :create => { + @reflection.primary_key_name => @owner.id + } + ) do + @reflection.klass.send(method, *args, &block) + end + end + end + + def find_target + if @reflection.options[:finder_sql] + @reflection.klass.find_by_sql(@finder_sql) + else + find(:all) + end + end + + def count_records + count = if has_cached_counter? + @owner.send(:read_attribute, cached_counter_attribute_name) + elsif @reflection.options[:counter_sql] + @reflection.klass.count_by_sql(@counter_sql) + else + @reflection.klass.count(@counter_sql) + end + + @target = [] and loaded if count == 0 + + if @reflection.options[:limit] + count = [ @reflection.options[:limit], count ].min + end + + return count + end + + def has_cached_counter? + @owner.attribute_present?(cached_counter_attribute_name) + end + + def cached_counter_attribute_name + "#{@reflection.name}_count" + end + + def insert_record(record) + set_belongs_to_association_for(record) + record.save + end + + def delete_records(records) + if @reflection.options[:dependent] + records.each { |r| r.destroy } + else + ids = quoted_record_ids(records) + @reflection.klass.update_all( + "#{@reflection.primary_key_name} = NULL", + "#{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})" + ) + end + end + + def target_obsolete? + false + end + + def construct_sql + case + when @reflection.options[:finder_sql] + @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) + + when @reflection.options[:as] + @finder_sql = + "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " + + "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote @owner.class.base_class.name.to_s}" + @finder_sql << " AND (#{conditions})" if conditions + + else + @finder_sql = "#{@reflection.klass.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}" + @finder_sql << " AND (#{conditions})" if conditions + end + + if @reflection.options[:counter_sql] + @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) + elsif @reflection.options[:finder_sql] + # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ + @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } + @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) + else + @counter_sql = @finder_sql + end + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/associations/has_many_through_association.rb b/vendor/rails/activerecord/lib/active_record/associations/has_many_through_association.rb new file mode 100644 index 00000000..44054b42 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -0,0 +1,147 @@ +module ActiveRecord + module Associations + class HasManyThroughAssociation < AssociationProxy #:nodoc: + def initialize(owner, reflection) + super + reflection.check_validity! + @finder_sql = construct_conditions + construct_sql + end + + + def find(*args) + options = Base.send(:extract_options_from_args!, args) + + conditions = "#{@finder_sql}" + if sanitized_conditions = sanitize_sql(options[:conditions]) + conditions << " AND (#{sanitized_conditions})" + end + options[:conditions] = conditions + + if options[:order] && @reflection.options[:order] + options[:order] = "#{options[:order]}, #{@reflection.options[:order]}" + elsif @reflection.options[:order] + options[:order] = @reflection.options[:order] + end + + options[:select] = construct_select(options[:select]) + options[:from] ||= construct_from + options[:joins] = construct_joins(options[:joins]) + options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? + + merge_options_from_reflection!(options) + + # Pass through args exactly as we received them. + args << options + @reflection.klass.find(*args) + end + + def reset + @target = [] + @loaded = false + end + + protected + def method_missing(method, *args, &block) + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) + super + else + @reflection.klass.with_scope(construct_scope) { @reflection.klass.send(method, *args, &block) } + end + end + + def find_target + @reflection.klass.find(:all, + :select => construct_select, + :conditions => construct_conditions, + :from => construct_from, + :joins => construct_joins, + :order => @reflection.options[:order], + :limit => @reflection.options[:limit], + :group => @reflection.options[:group], + :include => @reflection.options[:include] || @reflection.source_reflection.options[:include] + ) + end + + def construct_conditions + conditions = if @reflection.through_reflection.options[:as] + "#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.options[:as]}_id = #{@owner.quoted_id} " + + "AND #{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.options[:as]}_type = #{@owner.class.quote @owner.class.base_class.name.to_s}" + else + "#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.primary_key_name} = #{@owner.quoted_id}" + end + conditions << " AND (#{sql_conditions})" if sql_conditions + + return conditions + end + + def construct_from + @reflection.table_name + end + + def construct_select(custom_select = nil) + selected = custom_select || @reflection.options[:select] || "#{@reflection.table_name}.*" + end + + def construct_joins(custom_joins = nil) + polymorphic_join = nil + if @reflection.through_reflection.options[:as] || @reflection.source_reflection.macro == :belongs_to + reflection_primary_key = @reflection.klass.primary_key + source_primary_key = @reflection.source_reflection.primary_key_name + else + reflection_primary_key = @reflection.source_reflection.primary_key_name + source_primary_key = @reflection.klass.primary_key + if @reflection.source_reflection.options[:as] + polymorphic_join = "AND %s.%s = %s" % [ + @reflection.table_name, "#{@reflection.source_reflection.options[:as]}_type", + @owner.class.quote(@reflection.through_reflection.klass.name) + ] + end + end + + "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [ + @reflection.through_reflection.table_name, + @reflection.table_name, reflection_primary_key, + @reflection.through_reflection.table_name, source_primary_key, + polymorphic_join + ] + end + + def construct_scope + { + :find => { :from => construct_from, :conditions => construct_conditions, :joins => construct_joins, :select => construct_select }, + :create => { @reflection.primary_key_name => @owner.id } + } + end + + def construct_sql + case + when @reflection.options[:finder_sql] + @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) + + @finder_sql = "#{@reflection.klass.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}" + @finder_sql << " AND (#{conditions})" if conditions + end + + if @reflection.options[:counter_sql] + @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) + elsif @reflection.options[:finder_sql] + # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ + @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } + @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) + else + @counter_sql = @finder_sql + end + end + + def conditions + @conditions ||= [ + (interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions]), + (interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.through_reflection.options[:conditions])) if @reflection.through_reflection.options[:conditions]) + ].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions]) + end + + alias_method :sql_conditions, :conditions + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/associations/has_one_association.rb b/vendor/rails/activerecord/lib/active_record/associations/has_one_association.rb new file mode 100644 index 00000000..e881d494 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/associations/has_one_association.rb @@ -0,0 +1,80 @@ +module ActiveRecord + module Associations + class HasOneAssociation < BelongsToAssociation #:nodoc: + def initialize(owner, reflection) + super + construct_sql + end + + def create(attributes = {}, replace_existing = true) + record = build(attributes, replace_existing) + record.save + record + end + + def build(attributes = {}, replace_existing = true) + record = @reflection.klass.new(attributes) + + if replace_existing + replace(record, true) + else + record[@reflection.primary_key_name] = @owner.id unless @owner.new_record? + self.target = record + end + + record + end + + def replace(obj, dont_save = false) + load_target + + unless @target.nil? + if dependent? && !dont_save && @target != obj + @target.destroy unless @target.new_record? + @owner.clear_association_cache + else + @target[@reflection.primary_key_name] = nil + @target.save unless @owner.new_record? || @target.new_record? + end + end + + if obj.nil? + @target = nil + else + raise_on_type_mismatch(obj) + set_belongs_to_association_for(obj) + @target = (AssociationProxy === obj ? obj.target : obj) + end + + @loaded = true + + unless @owner.new_record? or obj.nil? or dont_save + return (obj.save ? self : false) + else + return (obj.nil? ? nil : self) + end + end + + private + def find_target + @reflection.klass.find(:first, + :conditions => @finder_sql, + :order => @reflection.options[:order], + :include => @reflection.options[:include] + ) + end + + def construct_sql + case + when @reflection.options[:as] + @finder_sql = + "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " + + "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote @owner.class.base_class.name.to_s}" + else + @finder_sql = "#{@reflection.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}" + end + @finder_sql << " AND (#{conditions})" if conditions + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/base.rb b/vendor/rails/activerecord/lib/active_record/base.rb new file mode 100755 index 00000000..6c3d1022 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/base.rb @@ -0,0 +1,2073 @@ +require 'yaml' +require 'set' +require 'active_record/deprecated_finders' + +module ActiveRecord #:nodoc: + class ActiveRecordError < StandardError #:nodoc: + end + class SubclassNotFound < ActiveRecordError #:nodoc: + end + class AssociationTypeMismatch < ActiveRecordError #:nodoc: + end + class SerializationTypeMismatch < ActiveRecordError #:nodoc: + end + class AdapterNotSpecified < ActiveRecordError # :nodoc: + end + class AdapterNotFound < ActiveRecordError # :nodoc: + end + class ConnectionNotEstablished < ActiveRecordError #:nodoc: + end + class ConnectionFailed < ActiveRecordError #:nodoc: + end + class RecordNotFound < ActiveRecordError #:nodoc: + end + class RecordNotSaved < ActiveRecordError #:nodoc: + end + class StatementInvalid < ActiveRecordError #:nodoc: + end + class PreparedStatementInvalid < ActiveRecordError #:nodoc: + end + class StaleObjectError < ActiveRecordError #:nodoc: + end + class ConfigurationError < StandardError #:nodoc: + end + class ReadOnlyRecord < StandardError #:nodoc: + end + + class AttributeAssignmentError < ActiveRecordError #:nodoc: + attr_reader :exception, :attribute + def initialize(message, exception, attribute) + @exception = exception + @attribute = attribute + @message = message + end + end + + class MultiparameterAssignmentErrors < ActiveRecordError #:nodoc: + attr_reader :errors + def initialize(errors) + @errors = errors + end + end + + # Active Record objects don't specify their attributes directly, but rather infer them from the table definition with + # which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change + # is instantly reflected in the Active Record objects. The mapping that binds a given Active Record class to a certain + # database table will happen automatically in most common cases, but can be overwritten for the uncommon ones. + # + # See the mapping rules in table_name and the full example in link:files/README.html for more insight. + # + # == Creation + # + # Active Records accept constructor parameters either in a hash or as a block. The hash method is especially useful when + # you're receiving the data from somewhere else, like a HTTP request. It works like this: + # + # user = User.new(:name => "David", :occupation => "Code Artist") + # user.name # => "David" + # + # You can also use block initialization: + # + # user = User.new do |u| + # u.name = "David" + # u.occupation = "Code Artist" + # end + # + # And of course you can just create a bare object and specify the attributes after the fact: + # + # user = User.new + # user.name = "David" + # user.occupation = "Code Artist" + # + # == Conditions + # + # Conditions can either be specified as a string or an array representing the WHERE-part of an SQL statement. + # The array form is to be used when the condition input is tainted and requires sanitization. The string form can + # be used for statements that don't involve tainted data. Examples: + # + # User < ActiveRecord::Base + # def self.authenticate_unsafely(user_name, password) + # find(:first, :conditions => "user_name = '#{user_name}' AND password = '#{password}'") + # end + # + # def self.authenticate_safely(user_name, password) + # find(:first, :conditions => [ "user_name = ? AND password = ?", user_name, password ]) + # end + # end + # + # The authenticate_unsafely method inserts the parameters directly into the query and is thus susceptible to SQL-injection + # attacks if the user_name and +password+ parameters come directly from a HTTP request. The authenticate_safely method, + # on the other hand, will sanitize the user_name and +password+ before inserting them in the query, which will ensure that + # an attacker can't escape the query and fake the login (or worse). + # + # When using multiple parameters in the conditions, it can easily become hard to read exactly what the fourth or fifth + # question mark is supposed to represent. In those cases, you can resort to named bind variables instead. That's done by replacing + # the question marks with symbols and supplying a hash with values for the matching symbol keys: + # + # Company.find(:first, [ + # "id = :id AND name = :name AND division = :division AND created_at > :accounting_date", + # { :id => 3, :name => "37signals", :division => "First", :accounting_date => '2005-01-01' } + # ]) + # + # == Overwriting default accessors + # + # All column values are automatically available through basic accessors on the Active Record object, but some times you + # want to specialize this behavior. This can be done by either by overwriting the default accessors (using the same + # name as the attribute) calling read_attribute(attr_name) and write_attribute(attr_name, value) to actually change things. + # Example: + # + # class Song < ActiveRecord::Base + # # Uses an integer of seconds to hold the length of the song + # + # def length=(minutes) + # write_attribute(:length, minutes * 60) + # end + # + # def length + # read_attribute(:length) / 60 + # end + # end + # + # You can alternatively use self[:attribute]=(value) and self[:attribute] instead of write_attribute(:attribute, vaule) and + # read_attribute(:attribute) as a shorter form. + # + # == Accessing attributes before they have been typecasted + # + # Sometimes you want to be able to read the raw attribute data without having the column-determined typecast run its course first. + # That can be done by using the _before_type_cast accessors that all attributes have. For example, if your Account model + # has a balance attribute, you can call account.balance_before_type_cast or account.id_before_type_cast. + # + # This is especially useful in validation situations where the user might supply a string for an integer field and you want to display + # the original string back in an error message. Accessing the attribute normally would typecast the string to 0, which isn't what you + # want. + # + # == Dynamic attribute-based finders + # + # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects by simple queries without turning to SQL. They work by + # appending the name of an attribute to find_by_ or find_all_by_, so you get finders like Person.find_by_user_name, + # Person.find_all_by_last_name, Payment.find_by_transaction_id. So instead of writing + # Person.find(:first, ["user_name = ?", user_name]), you just do Person.find_by_user_name(user_name). + # And instead of writing Person.find(:all, ["last_name = ?", last_name]), you just do Person.find_all_by_last_name(last_name). + # + # It's also possible to use multiple attributes in the same find by separating them with "_and_", so you get finders like + # Person.find_by_user_name_and_password or even Payment.find_by_purchaser_and_state_and_country. So instead of writing + # Person.find(:first, ["user_name = ? AND password = ?", user_name, password]), you just do + # Person.find_by_user_name_and_password(user_name, password). + # + # It's even possible to use all the additional parameters to find. For example, the full interface for Payment.find_all_by_amount + # is actually Payment.find_all_by_amount(amount, options). And the full interface to Person.find_by_user_name is + # actually Person.find_by_user_name(user_name, options). So you could call Payment.find_all_by_amount(50, :order => "created_on"). + # + # The same dynamic finder style can be used to create the object if it doesn't already exist. This dynamic finder is called with + # find_or_create_by_ and will return the object if it already exists and otherwise creates it, then returns it. Example: + # + # # No 'Summer' tag exists + # Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer") + # + # # Now the 'Summer' tag does exist + # Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer") + # + # == Saving arrays, hashes, and other non-mappable objects in text columns + # + # Active Record can serialize any object in text columns using YAML. To do so, you must specify this with a call to the class method +serialize+. + # This makes it possible to store arrays, hashes, and other non-mappable objects without doing any additional work. Example: + # + # class User < ActiveRecord::Base + # serialize :preferences + # end + # + # user = User.create(:preferences => { "background" => "black", "display" => large }) + # User.find(user.id).preferences # => { "background" => "black", "display" => large } + # + # You can also specify a class option as the second parameter that'll raise an exception if a serialized object is retrieved as a + # descendent of a class not in the hierarchy. Example: + # + # class User < ActiveRecord::Base + # serialize :preferences, Hash + # end + # + # user = User.create(:preferences => %w( one two three )) + # User.find(user.id).preferences # raises SerializationTypeMismatch + # + # == Single table inheritance + # + # Active Record allows inheritance by storing the name of the class in a column that by default is called "type" (can be changed + # by overwriting Base.inheritance_column). This means that an inheritance looking like this: + # + # class Company < ActiveRecord::Base; end + # class Firm < Company; end + # class Client < Company; end + # class PriorityClient < Client; end + # + # When you do Firm.create(:name => "37signals"), this record will be saved in the companies table with type = "Firm". You can then + # fetch this row again using Company.find(:first, "name = '37signals'") and it will return a Firm object. + # + # If you don't have a type column defined in your table, single-table inheritance won't be triggered. In that case, it'll work just + # like normal subclasses with no special magic for differentiating between them or reloading the right type with find. + # + # Note, all the attributes for all the cases are kept in the same table. Read more: + # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html + # + # == Connection to multiple databases in different models + # + # Connections are usually created through ActiveRecord::Base.establish_connection and retrieved by ActiveRecord::Base.connection. + # All classes inheriting from ActiveRecord::Base will use this connection. But you can also set a class-specific connection. + # For example, if Course is a ActiveRecord::Base, but resides in a different database you can just say Course.establish_connection + # and Course *and all its subclasses* will use this connection instead. + # + # This feature is implemented by keeping a connection pool in ActiveRecord::Base that is a Hash indexed by the class. If a connection is + # requested, the retrieve_connection method will go up the class-hierarchy until a connection is found in the connection pool. + # + # == Exceptions + # + # * +ActiveRecordError+ -- generic error class and superclass of all other errors raised by Active Record + # * +AdapterNotSpecified+ -- the configuration hash used in establish_connection didn't include a + # :adapter key. + # * +AdapterNotFound+ -- the :adapter key used in establish_connection specified an non-existent adapter + # (or a bad spelling of an existing one). + # * +AssociationTypeMismatch+ -- the object assigned to the association wasn't of the type specified in the association definition. + # * +SerializationTypeMismatch+ -- the object serialized wasn't of the class specified as the second parameter. + # * +ConnectionNotEstablished+ -- no connection has been established. Use establish_connection before querying. + # * +RecordNotFound+ -- no record responded to the find* method. + # Either the row with the given ID doesn't exist or the row didn't meet the additional restrictions. + # * +StatementInvalid+ -- the database server rejected the SQL statement. The precise error is added in the message. + # Either the record with the given ID doesn't exist or the record didn't meet the additional restrictions. + # * +MultiparameterAssignmentErrors+ -- collection of errors that occurred during a mass assignment using the + # +attributes=+ method. The +errors+ property of this exception contains an array of +AttributeAssignmentError+ + # objects that should be inspected to determine which attributes triggered the errors. + # * +AttributeAssignmentError+ -- an error occurred while doing a mass assignment through the +attributes=+ method. + # You can inspect the +attribute+ property of the exception object to determine which attribute triggered the error. + # + # *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level). + # So it's possible to assign a logger to the class through Base.logger= which will then be used by all + # instances in the current object space. + class Base + # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then passed + # on to any new database connections made and which can be retrieved on both a class and instance level by calling +logger+. + cattr_accessor :logger + + include Reloadable::Subclasses + + def self.inherited(child) #:nodoc: + @@subclasses[self] ||= [] + @@subclasses[self] << child + super + end + + def self.reset_subclasses #:nodoc: + nonreloadables = [] + subclasses.each do |klass| + unless klass.reloadable? + nonreloadables << klass + next + end + klass.instance_variables.each { |var| klass.send(:remove_instance_variable, var) } + klass.instance_methods(false).each { |m| klass.send :undef_method, m } + end + @@subclasses = {} + nonreloadables.each { |klass| (@@subclasses[klass.superclass] ||= []) << klass } + end + + @@subclasses = {} + + cattr_accessor :configurations + @@configurations = {} + + # Accessor for the prefix type that will be prepended to every primary key column name. The options are :table_name and + # :table_name_with_underscore. If the first is specified, the Product class will look for "productid" instead of "id" as + # the primary column. If the latter is specified, the Product class will look for "product_id" instead of "id". Remember + # that this is a global setting for all Active Records. + cattr_accessor :primary_key_prefix_type + @@primary_key_prefix_type = nil + + # Accessor for the name of the prefix string to prepend to every table name. So if set to "basecamp_", all + # table names will be named like "basecamp_projects", "basecamp_people", etc. This is a convenient way of creating a namespace + # for tables in a shared database. By default, the prefix is the empty string. + cattr_accessor :table_name_prefix + @@table_name_prefix = "" + + # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp", + # "people_basecamp"). By default, the suffix is the empty string. + cattr_accessor :table_name_suffix + @@table_name_suffix = "" + + # Indicates whether or not table names should be the pluralized versions of the corresponding class names. + # If true, the default table name for a +Product+ class will be +products+. If false, it would just be +product+. + # See table_name for the full rules on table/class naming. This is true, by default. + cattr_accessor :pluralize_table_names + @@pluralize_table_names = true + + # Determines whether or not to use ANSI codes to colorize the logging statements committed by the connection adapter. These colors + # make it much easier to overview things during debugging (when used through a reader like +tail+ and on a black background), but + # may complicate matters if you use software like syslog. This is true, by default. + cattr_accessor :colorize_logging + @@colorize_logging = true + + # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling dates and times from the database. + # This is set to :local by default. + cattr_accessor :default_timezone + @@default_timezone = :local + + # Determines whether or not to use a connection for each thread, or a single shared connection for all threads. + # Defaults to false. Set to true if you're writing a threaded application. + cattr_accessor :allow_concurrency + @@allow_concurrency = false + + # Determines whether to speed up access by generating optimized reader + # methods to avoid expensive calls to method_missing when accessing + # attributes by name. You might want to set this to false in development + # mode, because the methods would be regenerated on each request. + cattr_accessor :generate_read_methods + @@generate_read_methods = true + + # Specifies the format to use when dumping the database schema with Rails' + # Rakefile. If :sql, the schema is dumped as (potentially database- + # specific) SQL statements. If :ruby, the schema is dumped as an + # ActiveRecord::Schema file which can be loaded into any database that + # supports migrations. Use :ruby if you want to have different database + # adapters for, e.g., your development and test environments. + cattr_accessor :schema_format + @@schema_format = :ruby + + class << self # Class methods + # Find operates with three different retrieval approaches: + # + # * Find by id: This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). + # If no record can be found for all of the listed ids, then RecordNotFound will be raised. + # * Find first: This will return the first record matched by the options used. These options can either be specific + # conditions or merely an order. If no record can matched, nil is returned. + # * Find all: This will return all the records matched by the options used. If no records are found, an empty array is returned. + # + # All approaches accept an option hash as their last parameter. The options are: + # + # * :conditions: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro. + # * :order: An SQL fragment like "created_at DESC, name". + # * :group: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. + # * :limit: An integer determining the limit on the number of rows that should be returned. + # * :offset: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. + # * :joins: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). + # The records will be returned read-only since they will have attributes that do not correspond to the table's columns. + # Pass :readonly => false to override. + # * :include: Names associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer + # to already defined associations. See eager loading under Associations. + # * :select: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not + # include the joined columns. + # * :readonly: Mark the returned records read-only so they cannot be saved or updated. + # + # Examples for find by id: + # Person.find(1) # returns the object for ID = 1 + # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6) + # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17) + # Person.find([1]) # returns an array for objects the object with ID = 1 + # Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC") + # + # Examples for find first: + # Person.find(:first) # returns the first object fetched by SELECT * FROM people + # Person.find(:first, :conditions => [ "user_name = ?", user_name]) + # Person.find(:first, :order => "created_on DESC", :offset => 5) + # + # Examples for find all: + # Person.find(:all) # returns an array of objects for all the rows fetched by SELECT * FROM people + # Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50) + # Person.find(:all, :offset => 10, :limit => 10) + # Person.find(:all, :include => [ :account, :friends ]) + # Person.find(:all, :group => "category") + def find(*args) + options = extract_options_from_args!(args) + validate_find_options(options) + set_readonly_option!(options) + + case args.first + when :first then find_initial(options) + when :all then find_every(options) + else find_from_ids(args, options) + end + end + + # Works like find(:all), but requires a complete SQL string. Examples: + # Post.find_by_sql "SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id" + # Post.find_by_sql ["SELECT * FROM posts WHERE author = ? AND created > ?", author_id, start_date] + def find_by_sql(sql) + connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) } + end + + # Returns true if the given +id+ represents the primary key of a record in the database, false otherwise. + # Example: + # Person.exists?(5) + def exists?(id) + !find(:first, :conditions => ["#{primary_key} = ?", id]).nil? rescue false + end + + # Creates an object, instantly saves it as a record (if the validation permits it), and returns it. If the save + # fails under validations, the unsaved object is still returned. + def create(attributes = nil) + if attributes.is_a?(Array) + attributes.collect { |attr| create(attr) } + else + object = new(attributes) + scope(:create).each { |att,value| object.send("#{att}=", value) } if scoped?(:create) + object.save + object + end + end + + # Finds the record from the passed +id+, instantly saves it with the passed +attributes+ (if the validation permits it), + # and returns it. If the save fails under validations, the unsaved object is still returned. + # + # The arguments may also be given as arrays in which case the update method is called for each pair of +id+ and + # +attributes+ and an array of objects is returned. + # + # Example of updating one record: + # Person.update(15, {:user_name => 'Samuel', :group => 'expert'}) + # + # Example of updating multiple records: + # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} } + # Person.update(people.keys, people.values) + def update(id, attributes) + if id.is_a?(Array) + idx = -1 + id.collect { |id| idx += 1; update(id, attributes[idx]) } + else + object = find(id) + object.update_attributes(attributes) + object + end + end + + # Deletes the record with the given +id+ without instantiating an object first. If an array of ids is provided, all of them + # are deleted. + def delete(id) + delete_all([ "#{primary_key} IN (?)", id ]) + end + + # Destroys the record with the given +id+ by instantiating the object and calling #destroy (all the callbacks are the triggered). + # If an array of ids is provided, all of them are destroyed. + def destroy(id) + id.is_a?(Array) ? id.each { |id| destroy(id) } : find(id).destroy + end + + # Updates all records with the SET-part of an SQL update statement in +updates+ and returns an integer with the number of rows updated. + # A subset of the records can be selected by specifying +conditions+. Example: + # Billing.update_all "category = 'authorized', approved = 1", "author = 'David'" + def update_all(updates, conditions = nil) + sql = "UPDATE #{table_name} SET #{sanitize_sql(updates)} " + add_conditions!(sql, conditions, scope(:find)) + connection.update(sql, "#{name} Update") + end + + # Destroys the objects for all the records that match the +condition+ by instantiating each object and calling + # the destroy method. Example: + # Person.destroy_all "last_login < '2004-04-04'" + def destroy_all(conditions = nil) + find(:all, :conditions => conditions).each { |object| object.destroy } + end + + # Deletes all the records that match the +condition+ without instantiating the objects first (and hence not + # calling the destroy method). Example: + # Post.delete_all "person_id = 5 AND (category = 'Something' OR category = 'Else')" + def delete_all(conditions = nil) + sql = "DELETE FROM #{table_name} " + add_conditions!(sql, conditions, scope(:find)) + connection.delete(sql, "#{name} Delete all") + end + + # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. + # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" + def count_by_sql(sql) + sql = sanitize_conditions(sql) + connection.select_value(sql, "#{name} Count").to_i + end + + # Increments the specified counter by one. So DiscussionBoard.increment_counter("post_count", + # discussion_board_id) would increment the "post_count" counter on the board responding to discussion_board_id. + # This is used for caching aggregate values, so that they don't need to be computed every time. Especially important + # for looping over a collection where each element require a number of aggregate values. Like the DiscussionBoard + # that needs to list both the number of posts and comments. + def increment_counter(counter_name, id) + update_all "#{counter_name} = #{counter_name} + 1", "#{primary_key} = #{quote(id)}" + end + + # Works like increment_counter, but decrements instead. + def decrement_counter(counter_name, id) + update_all "#{counter_name} = #{counter_name} - 1", "#{primary_key} = #{quote(id)}" + end + + + # Attributes named in this macro are protected from mass-assignment, such as new(attributes) and + # attributes=(attributes). Their assignment will simply be ignored. Instead, you can use the direct writer + # methods to do assignment. This is meant to protect sensitive attributes from being overwritten by URL/form hackers. Example: + # + # class Customer < ActiveRecord::Base + # attr_protected :credit_rating + # end + # + # customer = Customer.new("name" => David, "credit_rating" => "Excellent") + # customer.credit_rating # => nil + # customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" } + # customer.credit_rating # => nil + # + # customer.credit_rating = "Average" + # customer.credit_rating # => "Average" + def attr_protected(*attributes) + write_inheritable_array("attr_protected", attributes - (protected_attributes || [])) + end + + # Returns an array of all the attributes that have been protected from mass-assignment. + def protected_attributes # :nodoc: + read_inheritable_attribute("attr_protected") + end + + # If this macro is used, only those attributes named in it will be accessible for mass-assignment, such as + # new(attributes) and attributes=(attributes). This is the more conservative choice for mass-assignment + # protection. If you'd rather start from an all-open default and restrict attributes as needed, have a look at + # attr_protected. + def attr_accessible(*attributes) + write_inheritable_array("attr_accessible", attributes - (accessible_attributes || [])) + end + + # Returns an array of all the attributes that have been made accessible to mass-assignment. + def accessible_attributes # :nodoc: + read_inheritable_attribute("attr_accessible") + end + + + # 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 + # object must be of that class on retrieval or +SerializationTypeMismatch+ will be raised. + def serialize(attr_name, class_name = Object) + serialized_attributes[attr_name.to_s] = class_name + end + + # Returns a hash of all the attributes that have been specified for serialization as keys and their class restriction as values. + def serialized_attributes + read_inheritable_attribute("attr_serialized") or write_inheritable_attribute("attr_serialized", {}) + end + + + # Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending + # directly from ActiveRecord. So if the hierarchy looks like: Reply < Message < ActiveRecord, then Message is used + # to guess the table name from even when called on Reply. The rules used to do the guess are handled by the Inflector class + # in Active Support, which knows almost all common English inflections (report a bug if your inflection isn't covered). + # + # Additionally, the class-level table_name_prefix is prepended to the table_name and the table_name_suffix is appended. + # So if you have "myapp_" as a prefix, the table name guess for an Account class becomes "myapp_accounts". + # + # You can also overwrite this class method to allow for unguessable links, such as a Mouse class with a link to a + # "mice" table. Example: + # + # class Mouse < ActiveRecord::Base + # set_table_name "mice" + # end + def table_name + reset_table_name + end + + def reset_table_name #:nodoc: + name = "#{table_name_prefix}#{undecorated_table_name(base_class.name)}#{table_name_suffix}" + set_table_name(name) + name + end + + # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the + # primary_key_prefix_type setting, though. + def primary_key + reset_primary_key + end + + def reset_primary_key #:nodoc: + key = 'id' + case primary_key_prefix_type + when :table_name + key = Inflector.foreign_key(base_class.name, false) + when :table_name_with_underscore + key = Inflector.foreign_key(base_class.name) + end + set_primary_key(key) + key + end + + # Defines the column name for use with single table inheritance -- can be overridden in subclasses. + def inheritance_column + "type" + end + + # Lazy-set the sequence name to the connection's default. This method + # is only ever called once since set_sequence_name overrides it. + def sequence_name #:nodoc: + reset_sequence_name + end + + def reset_sequence_name #:nodoc: + default = connection.default_sequence_name(table_name, primary_key) + set_sequence_name(default) + default + end + + # Sets the table name to use to the given value, or (if the value + # is nil or false) to the value returned by the given block. + # + # Example: + # + # class Project < ActiveRecord::Base + # set_table_name "project" + # end + def set_table_name(value = nil, &block) + define_attr_method :table_name, value, &block + end + alias :table_name= :set_table_name + + # Sets the name of the primary key column to use to the given value, + # or (if the value is nil or false) to the value returned by the given + # block. + # + # Example: + # + # class Project < ActiveRecord::Base + # set_primary_key "sysid" + # end + def set_primary_key(value = nil, &block) + define_attr_method :primary_key, value, &block + end + alias :primary_key= :set_primary_key + + # Sets the name of the inheritance column to use to the given value, + # or (if the value # is nil or false) to the value returned by the + # given block. + # + # Example: + # + # class Project < ActiveRecord::Base + # set_inheritance_column do + # original_inheritance_column + "_id" + # end + # end + def set_inheritance_column(value = nil, &block) + define_attr_method :inheritance_column, value, &block + end + alias :inheritance_column= :set_inheritance_column + + # Sets the name of the sequence to use when generating ids to the given + # value, or (if the value is nil or false) to the value returned by the + # given block. This is required for Oracle and is useful for any + # database which relies on sequences for primary key generation. + # + # If a sequence name is not explicitly set when using Oracle or Firebird, + # it will default to the commonly used pattern of: #{table_name}_seq + # + # If a sequence name is not explicitly set when using PostgreSQL, it + # will discover the sequence corresponding to your primary key for you. + # + # Example: + # + # class Project < ActiveRecord::Base + # set_sequence_name "projectseq" # default would have been "project_seq" + # end + def set_sequence_name(value = nil, &block) + define_attr_method :sequence_name, value, &block + end + alias :sequence_name= :set_sequence_name + + # Turns the +table_name+ back into a class name following the reverse rules of +table_name+. + def class_name(table_name = table_name) # :nodoc: + # remove any prefix and/or suffix from the table name + class_name = table_name[table_name_prefix.length..-(table_name_suffix.length + 1)].camelize + class_name = class_name.singularize if pluralize_table_names + class_name + end + + # Indicates whether the table associated with this class exists + def table_exists? + if connection.respond_to?(:tables) + connection.tables.include? table_name + else + # if the connection adapter hasn't implemented tables, there are two crude tests that can be + # used - see if getting column info raises an error, or if the number of columns returned is zero + begin + reset_column_information + columns.size > 0 + rescue ActiveRecord::StatementInvalid + false + end + end + end + + # Returns an array of column objects for the table associated with this class. + def columns + unless @columns + @columns = connection.columns(table_name, "#{name} Columns") + @columns.each {|column| column.primary = column.name == primary_key} + end + @columns + end + + # Returns an array of column objects for the table associated with this class. + def columns_hash + @columns_hash ||= columns.inject({}) { |hash, column| hash[column.name] = column; hash } + end + + # Returns an array of column names as strings. + def column_names + @column_names ||= columns.map { |column| column.name } + end + + # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", + # and columns used for single table inheritance have been removed. + def content_columns + @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } + end + + # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key + # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute + # is available. + def column_methods_hash #:nodoc: + @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr| + attr_name = attr.to_s + methods[attr.to_sym] = attr_name + methods["#{attr}=".to_sym] = attr_name + methods["#{attr}?".to_sym] = attr_name + methods["#{attr}_before_type_cast".to_sym] = attr_name + methods + end + end + + # Contains the names of the generated reader methods. + def read_methods #:nodoc: + @read_methods ||= Set.new + end + + # Resets all the cached information about columns, which will cause them to be reloaded on the next request. + def reset_column_information + read_methods.each { |name| undef_method(name) } + @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @read_methods = nil + end + + def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc: + subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information } + end + + # Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example: + # Person.human_attribute_name("first_name") # => "First name" + # Deprecated in favor of just calling "first_name".humanize + def human_attribute_name(attribute_key_name) #:nodoc: + attribute_key_name.humanize + end + + def descends_from_active_record? # :nodoc: + superclass == Base || !columns_hash.include?(inheritance_column) + end + + def quote(object) #:nodoc: + connection.quote(object) + end + + # Used to sanitize objects before they're used in an SELECT SQL-statement. Delegates to connection.quote. + def sanitize(object) #:nodoc: + connection.quote(object) + end + + # Log and benchmark multiple statements in a single block. Example: + # + # Project.benchmark("Creating project") do + # project = Project.create("name" => "stuff") + # project.create_manager("name" => "David") + # project.milestones << Milestone.find(:all) + # end + # + # The benchmark is only recorded if the current level of the logger matches the log_level, which makes it + # easy to include benchmarking statements in production software that will remain inexpensive because the benchmark + # will only be conducted if the log level is low enough. + # + # The logging of the multiple statements is turned off unless use_silence is set to false. + def benchmark(title, log_level = Logger::DEBUG, use_silence = true) + if logger && logger.level == log_level + result = nil + seconds = Benchmark.realtime { result = use_silence ? silence { yield } : yield } + logger.add(log_level, "#{title} (#{'%.5f' % seconds})") + result + else + yield + end + end + + # Silences the logger for the duration of the block. + def silence + old_logger_level, logger.level = logger.level, Logger::ERROR if logger + yield + ensure + logger.level = old_logger_level if logger + end + + # Scope parameters to method calls within the block. Takes a hash of method_name => parameters hash. + # method_name may be :find or :create. :find parameters may include the :conditions, :joins, + # :include, :offset, :limit, and :readonly options. :create parameters are an attributes hash. + # + # Article.with_scope(:find => { :conditions => "blog_id = 1" }, :create => { :blog_id => 1 }) do + # Article.find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1 + # a = Article.create(1) + # a.blog_id # => 1 + # end + # + # In nested scopings, all previous parameters are overwritten by inner rule + # except :conditions in :find, that are merged as hash. + # + # Article.with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }, :create => { :blog_id => 1 }) do + # Article.with_scope(:find => { :limit => 10}) + # Article.find(:all) # => SELECT * from articles WHERE blog_id = 1 LIMIT 10 + # end + # Article.with_scope(:find => { :conditions => "author_id = 3" }) + # Article.find(:all) # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1 + # end + # end + # + # You can ignore any previous scopings by using with_exclusive_scope method. + # + # Article.with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }) do + # Article.with_exclusive_scope(:find => { :limit => 10 }) + # Article.find(:all) # => SELECT * from articles LIMIT 10 + # end + # end + def with_scope(method_scoping = {}, action = :merge, &block) + method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping) + + # Dup first and second level of hash (method and params). + method_scoping = method_scoping.inject({}) do |hash, (method, params)| + hash[method] = (params == true) ? params : params.dup + hash + end + + method_scoping.assert_valid_keys([ :find, :create ]) + + if f = method_scoping[:find] + f.assert_valid_keys([ :conditions, :joins, :select, :include, :from, :offset, :limit, :readonly ]) + f[:readonly] = true if !f[:joins].blank? && !f.has_key?(:readonly) + end + + # Merge scopings + if action == :merge && current_scoped_methods + method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)| + case hash[method] + when Hash + if method == :find + (hash[method].keys + params.keys).uniq.each do |key| + merge = hash[method][key] && params[key] # merge if both scopes have the same key + if key == :conditions && merge + hash[method][key] = [params[key], hash[method][key]].collect{ |sql| "( %s )" % sanitize_sql(sql) }.join(" AND ") + elsif key == :include && merge + hash[method][key] = merge_includes(hash[method][key], params[key]).uniq + else + hash[method][key] = hash[method][key] || params[key] + end + end + else + hash[method] = params.merge(hash[method]) + end + else + hash[method] = params + end + hash + end + end + + self.scoped_methods << method_scoping + + begin + yield + ensure + self.scoped_methods.pop + end + end + + # Works like with_scope, but discards any nested properties. + def with_exclusive_scope(method_scoping = {}, &block) + with_scope(method_scoping, :overwrite, &block) + end + + # Overwrite the default class equality method to provide support for association proxies. + def ===(object) + object.is_a?(self) + end + + # Deprecated + def threaded_connections #:nodoc: + allow_concurrency + end + + # Deprecated + def threaded_connections=(value) #:nodoc: + self.allow_concurrency = value + end + + # Returns the base AR subclass that this class descends from. If A + # extends AR::Base, A.base_class will return A. If B descends from A + # through some arbitrarily deep hierarchy, B.base_class will return A. + def base_class + class_of_active_record_descendant(self) + end + + # Set this to true if this is an abstract class (see #abstract_class?). + attr_accessor :abstract_class + + # Returns whether this class is a base AR class. If A is a base class and + # B descends from A, then B.base_class will return B. + def abstract_class? + abstract_class == true + end + + private + def find_initial(options) + options.update(:limit => 1) unless options[:include] + find_every(options).first + end + + def find_every(options) + records = scoped?(:find, :include) || options[:include] ? + find_with_associations(options) : + find_by_sql(construct_finder_sql(options)) + + records.each { |record| record.readonly! } if options[:readonly] + + records + end + + def find_from_ids(ids, options) + expects_array = ids.first.kind_of?(Array) + return ids.first if expects_array && ids.first.empty? + + ids = ids.flatten.compact.uniq + + case ids.size + when 0 + raise RecordNotFound, "Couldn't find #{name} without an ID" + when 1 + result = find_one(ids.first, options) + expects_array ? [ result ] : result + else + find_some(ids, options) + end + end + + def find_one(id, options) + conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions] + options.update :conditions => "#{table_name}.#{primary_key} = #{sanitize(id)}#{conditions}" + + if result = find_initial(options) + result + else + raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}" + end + end + + def find_some(ids, options) + conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions] + ids_list = ids.map { |id| sanitize(id) }.join(',') + options.update :conditions => "#{table_name}.#{primary_key} IN (#{ids_list})#{conditions}" + + result = find_every(options) + + if result.size == ids.size + result + else + raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}" + end + end + + # Finder methods must instantiate through this method to work with the single-table inheritance model + # that makes it possible to create objects of different types from the same table. + def instantiate(record) + object = + if subclass_name = record[inheritance_column] + if subclass_name.empty? + allocate + else + require_association_class(subclass_name) + begin + compute_type(subclass_name).allocate + rescue NameError + raise SubclassNotFound, + "The single-table inheritance mechanism failed to locate the subclass: '#{record[inheritance_column]}'. " + + "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + + "Please rename this column if you didn't intend it to be used for storing the inheritance class " + + "or overwrite #{self.to_s}.inheritance_column to use another column for that information." + end + end + else + allocate + end + + object.instance_variable_set("@attributes", record) + object + end + + # Nest the type name in the same module as this class. + # Bar is "MyApp::Business::Bar" relative to MyApp::Business::Foo + def type_name_with_module(type_name) + (/^::/ =~ type_name) ? type_name : "#{parent.name}::#{type_name}" + end + + def construct_finder_sql(options) + scope = scope(:find) + sql = "SELECT #{(scope && scope[:select]) || options[:select] || '*'} " + sql << "FROM #{(scope && scope[:from]) || options[:from] || table_name} " + + add_joins!(sql, options, scope) + add_conditions!(sql, options[:conditions], scope) + + sql << " GROUP BY #{options[:group]} " if options[:group] + sql << " ORDER BY #{options[:order]} " if options[:order] + + add_limit!(sql, options, scope) + + sql + end + + # Merges includes so that the result is a valid +include+ + def merge_includes(first, second) + safe_to_array(first) + safe_to_array(second) + end + + # Object#to_a is deprecated, though it does have the desired behaviour + def safe_to_array(o) + case o + when NilClass + [] + when Array + o + else + [o] + end + end + + # The optional scope argument is for the current :find scope. + def add_limit!(sql, options, scope = :auto) + scope = scope(:find) if :auto == scope + if scope + options[:limit] ||= scope[:limit] + options[:offset] ||= scope[:offset] + end + connection.add_limit_offset!(sql, options) + end + + # The optional scope argument is for the current :find scope. + def add_joins!(sql, options, scope = :auto) + scope = scope(:find) if :auto == scope + join = (scope && scope[:joins]) || options[:joins] + sql << " #{join} " if join + end + + # Adds a sanitized version of +conditions+ to the +sql+ string. Note that the passed-in +sql+ string is changed. + # The optional scope argument is for the current :find scope. + def add_conditions!(sql, conditions, scope = :auto) + scope = scope(:find) if :auto == scope + segments = [] + segments << sanitize_sql(scope[:conditions]) if scope && scope[:conditions] + segments << sanitize_sql(conditions) unless conditions.nil? + segments << type_condition unless descends_from_active_record? + segments.compact! + sql << "WHERE (#{segments.join(") AND (")}) " unless segments.empty? + end + + def type_condition + quoted_inheritance_column = connection.quote_column_name(inheritance_column) + type_condition = subclasses.inject("#{table_name}.#{quoted_inheritance_column} = '#{name.demodulize}' ") do |condition, subclass| + condition << "OR #{table_name}.#{quoted_inheritance_column} = '#{subclass.name.demodulize}' " + end + + " (#{type_condition}) " + end + + # Guesses the table name, but does not decorate it with prefix and suffix information. + def undecorated_table_name(class_name = base_class.name) + table_name = Inflector.underscore(Inflector.demodulize(class_name)) + table_name = Inflector.pluralize(table_name) if pluralize_table_names + table_name + end + + # Enables dynamic finders like find_by_user_name(user_name) and find_by_user_name_and_password(user_name, password) that are turned into + # find(:first, :conditions => ["user_name = ?", user_name]) and find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password]) + # respectively. Also works for find(:all), but using find_all_by_amount(50) that are turned into find(:all, :conditions => ["amount = ?", 50]). + # + # 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). + def method_missing(method_id, *arguments) + 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) + + attribute_names = extract_attribute_names_from_match(match) + super unless all_attributes_exists?(attribute_names) + + conditions = construct_conditions_from_arguments(attribute_names, arguments) + + case extra_options = arguments[attribute_names.size] + when nil + options = { :conditions => conditions } + set_readonly_option!(options) + send(finder, options) + + when Hash + finder_options = extra_options.merge(:conditions => conditions) + validate_find_options(finder_options) + set_readonly_option!(finder_options) + + if extra_options[:conditions] + with_scope(:find => { :conditions => extra_options[:conditions] }) do + send(finder, finder_options) + end + else + send(finder, finder_options) + end + + else + send(deprecated_finder, conditions, *arguments[attribute_names.length..-1]) # deprecated API + end + elsif match = /find_or_create_by_([_a-zA-Z]\w*)/.match(method_id.to_s) + attribute_names = extract_attribute_names_from_match(match) + super unless all_attributes_exists?(attribute_names) + + options = { :conditions => construct_conditions_from_arguments(attribute_names, arguments) } + set_readonly_option!(options) + find_initial(options) || create(construct_attributes_from_arguments(attribute_names, arguments)) + else + super + end + end + + def determine_finder(match) + match.captures.first == 'all_by' ? :find_every : :find_initial + end + + def determine_deprecated_finder(match) + match.captures.first == 'all_by' ? :find_all : :find_first + end + + def extract_attribute_names_from_match(match) + match.captures.last.split('_and_') + end + + def construct_conditions_from_arguments(attribute_names, arguments) + conditions = [] + attribute_names.each_with_index { |name, idx| conditions << "#{table_name}.#{connection.quote_column_name(name)} #{attribute_condition(arguments[idx])} " } + [ conditions.join(" AND "), *arguments[0...attribute_names.length] ] + end + + def construct_attributes_from_arguments(attribute_names, arguments) + attributes = {} + attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] } + attributes + end + + def all_attributes_exists?(attribute_names) + attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) } + end + + def attribute_condition(argument) + case argument + when nil then "IS ?" + when Array then "IN (?)" + else "= ?" + end + end + + # Defines an "attribute" method (like #inheritance_column or + # #table_name). A new (class) method will be created with the + # given name. If a value is specified, the new method will + # return that value (as a string). Otherwise, the given block + # will be used to compute the value of the method. + # + # The original method will be aliased, with the new name being + # prefixed with "original_". This allows the new method to + # access the original value. + # + # Example: + # + # class A < ActiveRecord::Base + # define_attr_method :primary_key, "sysid" + # define_attr_method( :inheritance_column ) do + # original_inheritance_column + "_id" + # end + # end + def define_attr_method(name, value=nil, &block) + sing = class << self; self; end + sing.send :alias_method, "original_#{name}", name + if block_given? + sing.send :define_method, name, &block + else + # use eval instead of a block to work around a memory leak in dev + # mode in fcgi + sing.class_eval "def #{name}; #{value.to_s.inspect}; end" + end + end + + protected + def subclasses #:nodoc: + @@subclasses[self] ||= [] + @@subclasses[self] + extra = @@subclasses[self].inject([]) {|list, subclass| list + subclass.subclasses } + end + + # Test whether the given method and optional key are scoped. + def scoped?(method, key = nil) #:nodoc: + if current_scoped_methods && (scope = current_scoped_methods[method]) + !key || scope.has_key?(key) + end + end + + # Retrieve the scope for the given method and optional key. + def scope(method, key = nil) #:nodoc: + if current_scoped_methods && (scope = current_scoped_methods[method]) + key ? scope[key] : scope + end + end + + def thread_safe_scoped_methods #:nodoc: + scoped_methods = (Thread.current[:scoped_methods] ||= {}) + scoped_methods[self] ||= [] + end + + def single_threaded_scoped_methods #:nodoc: + @scoped_methods ||= [] + end + + # pick up the correct scoped_methods version from @@allow_concurrency + if @@allow_concurrency + alias_method :scoped_methods, :thread_safe_scoped_methods + else + alias_method :scoped_methods, :single_threaded_scoped_methods + end + + def current_scoped_methods #:nodoc: + scoped_methods.last + end + + # Returns the class type of the record using the current module as a prefix. So descendents of + # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. + def compute_type(type_name) + modularized_name = type_name_with_module(type_name) + begin + instance_eval(modularized_name) + rescue NameError => e + instance_eval(type_name) + end + end + + # Returns the class descending directly from ActiveRecord in the inheritance hierarchy. + def class_of_active_record_descendant(klass) + if klass.superclass == Base || klass.superclass.abstract_class? + klass + elsif klass.superclass.nil? + raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" + else + class_of_active_record_descendant(klass.superclass) + end + end + + # Returns the name of the class descending directly from ActiveRecord in the inheritance hierarchy. + def class_name_of_active_record_descendant(klass) #:nodoc: + klass.base_class.name + end + + # Accepts an array or string. The string is returned untouched, but the array has each value + # sanitized and interpolated into the sql statement. + # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" + def sanitize_sql(ary) + return ary unless ary.is_a?(Array) + + statement, *values = ary + if values.first.is_a?(Hash) and statement =~ /:\w+/ + replace_named_bind_variables(statement, values.first) + elsif statement.include?('?') + replace_bind_variables(statement, values) + else + statement % values.collect { |value| connection.quote_string(value.to_s) } + end + end + + alias_method :sanitize_conditions, :sanitize_sql + + def replace_bind_variables(statement, values) #:nodoc: + raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) + bound = values.dup + statement.gsub('?') { quote_bound_value(bound.shift) } + end + + def replace_named_bind_variables(statement, bind_vars) #:nodoc: + statement.gsub(/:(\w+)/) do + match = $1.to_sym + if bind_vars.include?(match) + quote_bound_value(bind_vars[match]) + else + raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}" + end + end + end + + def quote_bound_value(value) #:nodoc: + if (value.respond_to?(:map) && !value.is_a?(String)) + value.map { |v| connection.quote(v) }.join(',') + else + connection.quote(value) + end + end + + def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc: + unless expected == provided + raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}" + end + end + + def extract_options_from_args!(args) #:nodoc: + args.last.is_a?(Hash) ? args.pop : {} + end + + VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, + :order, :select, :readonly, :group, :from ] + + def validate_find_options(options) #:nodoc: + options.assert_valid_keys(VALID_FIND_OPTIONS) + end + + def set_readonly_option!(options) #:nodoc: + # Inherit :readonly from finder scope if set. Otherwise, + # if :joins is not blank then :readonly defaults to true. + unless options.has_key?(:readonly) + if scoped?(:find, :readonly) + options[:readonly] = scope(:find, :readonly) + elsif !options[:joins].blank? && !options[:select] + options[:readonly] = true + end + end + end + + def encode_quoted_value(value) #:nodoc: + quoted_value = connection.quote(value) + quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'") # (for ruby mode) " + quoted_value + end + end + + public + # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with + # attributes but not yet saved (pass a hash with key names matching the associated table column names). + # In both instances, valid attribute keys are determined by the column names of the associated table -- + # hence you can't have attributes that aren't part of the table columns. + def initialize(attributes = nil) + @attributes = attributes_from_column_definition + @new_record = true + ensure_proper_type + self.attributes = attributes unless attributes.nil? + yield self if block_given? + end + + # A model instance's primary key is always available as model.id + # whether you name it the default 'id' or set it to something else. + def id + attr_name = self.class.primary_key + column = column_for_attribute(attr_name) + define_read_method(:id, attr_name, column) if self.class.generate_read_methods + read_attribute(attr_name) + end + + # Enables Active Record objects to be used as URL parameters in Action Pack automatically. + alias_method :to_param, :id + + def id_before_type_cast #:nodoc: + read_attribute_before_type_cast(self.class.primary_key) + end + + def quoted_id #:nodoc: + quote(id, column_for_attribute(self.class.primary_key)) + end + + # Sets the primary ID. + def id=(value) + write_attribute(self.class.primary_key, value) + end + + # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet. + def new_record? + @new_record + end + + # * No record exists: Creates a new record with values matching those of the object attributes. + # * A record does exist: Updates the record with values matching those of the object attributes. + def save + raise ReadOnlyRecord if readonly? + create_or_update + end + + # Attempts to save the record, but instead of just returning false if it couldn't happen, it raises a + # RecordNotSaved exception + def save! + save || raise(RecordNotSaved) + end + + # Deletes the record in the database and freezes this instance to reflect that no changes should + # be made (since they can't be persisted). + def destroy + unless new_record? + connection.delete <<-end_sql, "#{self.class.name} Destroy" + DELETE FROM #{self.class.table_name} + WHERE #{self.class.primary_key} = #{quoted_id} + end_sql + end + + freeze + end + + # Returns a clone of the record that hasn't been assigned an id yet and + # is treated as a new record. Note that this is a "shallow" clone: + # it copies the object's attributes only, not its associations. + # The extent of a "deep" clone is application-specific and is therefore + # left to the application to implement according to its need. + def clone + attrs = self.attributes_before_type_cast + attrs.delete(self.class.primary_key) + self.class.new do |record| + record.send :instance_variable_set, '@attributes', attrs + end + end + + # Updates a single attribute and saves the record. This is especially useful for boolean flags on existing records. + # Note: This method is overwritten by the Validation module that'll make sure that updates made with this method + # doesn't get subjected to validation checks. Hence, attributes can be updated even if the full object isn't valid. + def update_attribute(name, value) + send(name.to_s + '=', value) + save + end + + # Updates all the attributes from the passed-in Hash and saves the record. If the object is invalid, the saving will + # fail and false will be returned. + def update_attributes(attributes) + self.attributes = attributes + save + end + + # Initializes the +attribute+ to zero if nil and adds one. Only makes sense for number-based attributes. Returns self. + def increment(attribute) + self[attribute] ||= 0 + self[attribute] += 1 + self + end + + # Increments the +attribute+ and saves the record. + def increment!(attribute) + increment(attribute).update_attribute(attribute, self[attribute]) + end + + # Initializes the +attribute+ to zero if nil and subtracts one. Only makes sense for number-based attributes. Returns self. + def decrement(attribute) + self[attribute] ||= 0 + self[attribute] -= 1 + self + end + + # Decrements the +attribute+ and saves the record. + def decrement!(attribute) + decrement(attribute).update_attribute(attribute, self[attribute]) + end + + # Turns an +attribute+ that's currently true into false and vice versa. Returns self. + def toggle(attribute) + self[attribute] = !send("#{attribute}?") + self + end + + # Toggles the +attribute+ and saves the record. + def toggle!(attribute) + toggle(attribute).update_attribute(attribute, self[attribute]) + end + + # Reloads the attributes of this object from the database. + def reload + clear_aggregation_cache + clear_association_cache + @attributes.update(self.class.find(self.id).instance_variable_get('@attributes')) + self + end + + # Returns the value of the attribute identified by attr_name after it has been typecast (for example, + # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). + # (Alias for the protected read_attribute method). + def [](attr_name) + read_attribute(attr_name) + end + + # Updates the attribute identified by attr_name with the specified +value+. + # (Alias for the protected write_attribute method). + def []=(attr_name, value) + write_attribute(attr_name, value) + end + + # Allows you to set all the attributes at once by passing in a hash with keys + # matching the attribute names (which again matches the column names). Sensitive attributes can be protected + # from this form of mass-assignment by using the +attr_protected+ macro. Or you can alternatively + # specify which attributes *can* be accessed in with the +attr_accessible+ macro. Then all the + # attributes not included in that won't be allowed to be mass-assigned. + def attributes=(new_attributes) + return if new_attributes.nil? + attributes = new_attributes.dup + attributes.stringify_keys! + + multi_parameter_attributes = [] + remove_attributes_protected_from_mass_assignment(attributes).each do |k, v| + k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v) + end + + assign_multiparameter_attributes(multi_parameter_attributes) + end + + + # Returns a hash of all the attributes with their names as keys and clones of their objects as values. + def attributes(options = nil) + attributes = clone_attributes :read_attribute + + if options.nil? + attributes + else + if except = options[:except] + except = Array(except).collect { |attribute| attribute.to_s } + except.each { |attribute_name| attributes.delete(attribute_name) } + attributes + elsif only = options[:only] + only = Array(only).collect { |attribute| attribute.to_s } + attributes.delete_if { |key, value| !only.include?(key) } + attributes + else + raise ArgumentError, "Options does not specify :except or :only (#{options.keys.inspect})" + end + end + end + + # Returns a hash of cloned attributes before typecasting and deserialization. + def attributes_before_type_cast + clone_attributes :read_attribute_before_type_cast + end + + # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither + # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings). + def attribute_present?(attribute) + value = read_attribute(attribute) + !value.blank? or value == 0 + end + + # Returns true if the given attribute is in the attributes hash + def has_attribute?(attr_name) + @attributes.has_key?(attr_name.to_s) + end + + # Returns an array of names for the attributes available on this object sorted alphabetically. + def attribute_names + @attributes.keys.sort + end + + # Returns the column object for the named attribute. + def column_for_attribute(name) + self.class.columns_hash[name.to_s] + end + + # Returns true if the +comparison_object+ is the same object, or is of the same type and has the same id. + def ==(comparison_object) + comparison_object.equal?(self) || + (comparison_object.instance_of?(self.class) && + comparison_object.id == id && + !comparison_object.new_record?) + end + + # Delegates to == + def eql?(comparison_object) + self == (comparison_object) + end + + # Delegates to id in order to allow two records of the same type and id to work with something like: + # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] + def hash + id.hash + end + + # For checking respond_to? without searching the attributes (which is faster). + alias_method :respond_to_without_attributes?, :respond_to? + + # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and + # person.respond_to?("name?") which will all return true. + def respond_to?(method, include_priv = false) + if @attributes.nil? + return super + elsif attr_name = self.class.column_methods_hash[method.to_sym] + return true if @attributes.include?(attr_name) || attr_name == self.class.primary_key + return false if self.class.read_methods.include?(attr_name) + elsif @attributes.include?(method_name = method.to_s) + return true + elsif md = /(=|\?|_before_type_cast)$/.match(method_name) + return true if @attributes.include?(md.pre_match) + end + # super must be called at the end of the method, because the inherited respond_to? + # would return true for generated readers, even if the attribute wasn't present + super + end + + # Just freeze the attributes hash, such that associations are still accessible even on destroyed records. + def freeze + @attributes.freeze; self + end + + def frozen? + @attributes.frozen? + end + + # Records loaded through joins with piggy-back attributes will be marked as read only as they cannot be saved and return true to this query. + def readonly? + @readonly == true + end + + def readonly! #:nodoc: + @readonly = true + end + + # Builds an XML document to represent the model. Some configuration is + # availble through +options+, however more complicated cases should use + # Builder. + # + # By default the generated XML document will include the processing + # instruction and all object's attributes. For example: + # + # + # + # The First Topic + # David + # 1 + # false + # 0 + # 2000-01-01T08:28:00+12:00 + # 2003-07-16T09:28:00+1200 + # Have a nice day + # david@loudthinking.com + # + # 2004-04-15 + # + # + # This behaviour can be controlled with :only, :except, and :skip_instruct + # for instance: + # + # topic.to_xml(:skip_instruct => true, :except => [ :id, bonus_time, :written_on, replies_count ]) + # + # + # The First Topic + # David + # false + # Have a nice day + # david@loudthinking.com + # + # 2004-04-15 + # + # + # To include first level associations use :include + # + # firm.to_xml :include => [ :account, :clients ] + # + # + # + # 1 + # 1 + # 37signals + # + # + # 1 + # Summit + # + # + # 1 + # Microsoft + # + # + # + # 1 + # 50 + # + # + def to_xml(options = {}) + options[:root] ||= self.class.to_s.underscore + options[:except] = Array(options[:except]) << self.class.inheritance_column unless options[:only] # skip type column + root_only_or_except = { :only => options[:only], :except => options[:except] } + + attributes_for_xml = attributes(root_only_or_except) + + if include_associations = options.delete(:include) + include_has_options = include_associations.is_a?(Hash) + + for association in include_has_options ? include_associations.keys : Array(include_associations) + association_options = include_has_options ? include_associations[association] : root_only_or_except + + case self.class.reflect_on_association(association).macro + when :has_many, :has_and_belongs_to_many + records = send(association).to_a + unless records.empty? + attributes_for_xml[association] = records.collect do |record| + record.attributes(association_options) + end + end + when :has_one, :belongs_to + if record = send(association) + attributes_for_xml[association] = record.attributes(association_options) + end + end + end + end + + attributes_for_xml.to_xml(options) + end + + private + def create_or_update + if new_record? then create else update end + end + + # Updates the associated record with values matching those of the instance attributes. + def update + connection.update( + "UPDATE #{self.class.table_name} " + + "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " + + "WHERE #{self.class.primary_key} = #{quote(id)}", + "#{self.class.name} Update" + ) + + return true + end + + # Creates a new record with values matching those of the instance attributes. + def create + if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name) + self.id = connection.next_sequence_value(self.class.sequence_name) + end + + self.id = connection.insert( + "INSERT INTO #{self.class.table_name} " + + "(#{quoted_column_names.join(', ')}) " + + "VALUES(#{attributes_with_quotes.values.join(', ')})", + "#{self.class.name} Create", + self.class.primary_key, self.id, self.class.sequence_name + ) + + @new_record = false + + return true + end + + # Sets the attribute used for single table inheritance to this class name if this is not the ActiveRecord descendent. + # Considering the hierarchy Reply < Message < ActiveRecord, this makes it possible to do Reply.new without having to + # set Reply[Reply.inheritance_column] = "Reply" yourself. No such attribute would be set for objects of the + # Message class in that example. + def ensure_proper_type + unless self.class.descends_from_active_record? + write_attribute(self.class.inheritance_column, Inflector.demodulize(self.class.name)) + end + end + + # Allows access to the object attributes, which are held in the @attributes hash, as were + # they first-class methods. So a Person class with a name attribute can use Person#name and + # Person#name= and never directly use the attributes hash -- except for multiple assigns with + # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that + # the completed attribute is not nil or 0. + # + # It's also possible to instantiate related objects, so a Client class belonging to the clients + # table with a master_id foreign key can instantiate master through Client#master. + def method_missing(method_id, *args, &block) + method_name = method_id.to_s + if @attributes.include?(method_name) or + (md = /\?$/.match(method_name) and + @attributes.include?(method_name = md.pre_match)) + define_read_methods if self.class.read_methods.empty? && self.class.generate_read_methods + md ? query_attribute(method_name) : read_attribute(method_name) + elsif self.class.primary_key.to_s == method_name + id + elsif md = /(=|_before_type_cast)$/.match(method_name) + attribute_name, method_type = md.pre_match, md.to_s + if @attributes.include?(attribute_name) + case method_type + when '=' + write_attribute(attribute_name, args.first) + when '_before_type_cast' + read_attribute_before_type_cast(attribute_name) + end + else + super + end + else + super + end + end + + # Returns the value of the attribute identified by attr_name after it has been typecast (for example, + # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). + def read_attribute(attr_name) + attr_name = attr_name.to_s + if !(value = @attributes[attr_name]).nil? + if column = column_for_attribute(attr_name) + if unserializable_attribute?(attr_name, column) + unserialize_attribute(attr_name) + else + column.type_cast(value) + end + else + value + end + else + nil + end + end + + def read_attribute_before_type_cast(attr_name) + @attributes[attr_name] + end + + # Called on first read access to any given column and generates reader + # methods for all columns in the columns_hash if + # ActiveRecord::Base.generate_read_methods is set to true. + def define_read_methods + self.class.columns_hash.each do |name, column| + unless self.class.serialized_attributes[name] + define_read_method(name.to_sym, name, column) unless respond_to_without_attributes?(name) + define_question_method(name) unless respond_to_without_attributes?("#{name}?") + end + end + end + + # Define an attribute reader method. Cope with nil column. + def define_read_method(symbol, attr_name, column) + cast_code = column.type_cast_code('v') if column + access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" + + unless attr_name.to_s == self.class.primary_key.to_s + access_code = access_code.insert(0, "raise NoMethodError, 'missing attribute: #{attr_name}', caller unless @attributes.has_key?('#{attr_name}'); ") + self.class.read_methods << attr_name + end + + evaluate_read_method attr_name, "def #{symbol}; #{access_code}; end" + end + + # Define an attribute ? method. + def define_question_method(attr_name) + unless attr_name.to_s == self.class.primary_key.to_s + self.class.read_methods << "#{attr_name}?" + end + + evaluate_read_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end" + end + + # Evaluate the definition for an attribute reader or ? method + def evaluate_read_method(attr_name, method_definition) + begin + self.class.class_eval(method_definition) + rescue SyntaxError => err + self.class.read_methods.delete(attr_name) + if logger + logger.warn "Exception occured during reader method compilation." + logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?" + logger.warn "#{err.message}" + end + end + end + + # Returns true if the attribute is of a text column and marked for serialization. + def unserializable_attribute?(attr_name, column) + column.text? && self.class.serialized_attributes[attr_name] + end + + # Returns the unserialized object of the attribute. + def unserialize_attribute(attr_name) + unserialized_object = object_from_yaml(@attributes[attr_name]) + + if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) + @attributes[attr_name] = unserialized_object + else + raise SerializationTypeMismatch, + "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}" + end + end + + # Updates the attribute identified by attr_name with the specified +value+. Empty strings for fixnum and float + # columns are turned into nil. + def write_attribute(attr_name, value) + attr_name = attr_name.to_s + if (column = column_for_attribute(attr_name)) && column.number? + @attributes[attr_name] = convert_number_column_value(value) + else + @attributes[attr_name] = value + end + end + + def convert_number_column_value(value) + case value + when FalseClass: 0 + when TrueClass: 1 + when '': nil + else value + end + end + + def query_attribute(attr_name) + attribute = @attributes[attr_name] + if attribute.kind_of?(Fixnum) && attribute == 0 + false + elsif attribute.kind_of?(String) && attribute == "0" + false + elsif attribute.kind_of?(String) && attribute.empty? + false + elsif attribute.nil? + false + elsif attribute == false + false + elsif attribute == "f" + false + elsif attribute == "false" + false + else + true + end + end + + def remove_attributes_protected_from_mass_assignment(attributes) + if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil? + attributes.reject { |key, value| attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) } + elsif self.class.protected_attributes.nil? + attributes.reject { |key, value| !self.class.accessible_attributes.include?(key.gsub(/\(.+/, "").intern) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) } + elsif self.class.accessible_attributes.nil? + attributes.reject { |key, value| self.class.protected_attributes.include?(key.gsub(/\(.+/,"").intern) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) } + end + end + + # The primary key and inheritance column can never be set by mass-assignment for security reasons. + def attributes_protected_by_default + default = [ self.class.primary_key, self.class.inheritance_column ] + default << 'id' unless self.class.primary_key.eql? 'id' + default + end + + # Returns copy of the attributes hash where all the values have been safely quoted for use in + # an SQL statement. + def attributes_with_quotes(include_primary_key = true) + attributes.inject({}) do |quoted, (name, value)| + if column = column_for_attribute(name) + quoted[name] = quote(value, column) unless !include_primary_key && column.primary + end + quoted + end + end + + # Quote strings appropriately for SQL statements. + def quote(value, column = nil) + self.class.connection.quote(value, column) + end + + # Interpolate custom sql string in instance context. + # Optional record argument is meant for custom insert_sql. + def interpolate_sql(sql, record = nil) + instance_eval("%@#{sql.gsub('@', '\@')}@") + end + + # Initializes the attributes array with keys matching the columns from the linked table and + # the values matching the corresponding default value of that column, so + # that a new instance, or one populated from a passed-in Hash, still has all the attributes + # that instances loaded from the database would. + def attributes_from_column_definition + self.class.columns.inject({}) do |attributes, column| + attributes[column.name] = column.default unless column.name == self.class.primary_key + attributes + end + end + + # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done + # by calling new on the column type or aggregation type (through composed_of) object with these parameters. + # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate + # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the + # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, f for Float, + # s for String, and a for Array. If all the values for a given attribute is empty, the attribute will be set to nil. + def assign_multiparameter_attributes(pairs) + execute_callstack_for_multiparameter_attributes( + extract_callstack_for_multiparameter_attributes(pairs) + ) + end + + # Includes an ugly hack for Time.local instead of Time.new because the latter is reserved by Time itself. + def execute_callstack_for_multiparameter_attributes(callstack) + errors = [] + callstack.each do |name, values| + klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass + if values.empty? + send(name + "=", nil) + else + begin + send(name + "=", Time == klass ? klass.local(*values) : klass.new(*values)) + rescue => ex + errors << AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name) + end + end + end + unless errors.empty? + raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes" + end + end + + def extract_callstack_for_multiparameter_attributes(pairs) + attributes = { } + + for pair in pairs + multiparameter_name, value = pair + attribute_name = multiparameter_name.split("(").first + attributes[attribute_name] = [] unless attributes.include?(attribute_name) + + unless value.empty? + attributes[attribute_name] << + [ find_parameter_position(multiparameter_name), type_cast_attribute_value(multiparameter_name, value) ] + end + end + + attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } } + end + + def type_cast_attribute_value(multiparameter_name, value) + multiparameter_name =~ /\([0-9]*([a-z])\)/ ? value.send("to_" + $1) : value + end + + def find_parameter_position(multiparameter_name) + multiparameter_name.scan(/\(([0-9]*).*\)/).first.first + end + + # Returns a comma-separated pair list, like "key1 = val1, key2 = val2". + def comma_pair_list(hash) + hash.inject([]) { |list, pair| list << "#{pair.first} = #{pair.last}" }.join(", ") + end + + def quoted_column_names(attributes = attributes_with_quotes) + attributes.keys.collect do |column_name| + self.class.connection.quote_column_name(column_name) + end + end + + def quote_columns(quoter, hash) + hash.inject({}) do |quoted, (name, value)| + quoted[quoter.quote_column_name(name)] = value + quoted + end + end + + def quoted_comma_pair_list(quoter, hash) + comma_pair_list(quote_columns(quoter, hash)) + end + + def object_from_yaml(string) + return string unless string.is_a?(String) + YAML::load(string) rescue string + end + + def clone_attributes(reader_method = :read_attribute, attributes = {}) + self.attribute_names.inject(attributes) do |attributes, name| + attributes[name] = clone_attribute_value(reader_method, name) + attributes + end + end + + def clone_attribute_value(reader_method, attribute_name) + value = send(reader_method, attribute_name) + value.clone + rescue TypeError, NoMethodError + value + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/calculations.rb b/vendor/rails/activerecord/lib/active_record/calculations.rb new file mode 100644 index 00000000..863aedbf --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/calculations.rb @@ -0,0 +1,229 @@ +module ActiveRecord + module Calculations #:nodoc: + CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset] + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + # Count operates using three different approaches. + # + # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model. + # * Count by conditions or joins: For backwards compatibility, you can pass in +conditions+ and +joins+ as individual parameters. + # * Count using options will find the row count matched by the options used. + # + # The last approach, count using options, accepts an option hash as the only parameter. The options are: + # + # * :conditions: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro. + # * :joins: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). + # The records will be returned read-only since they will have attributes that do not correspond to the table's columns. + # * :include: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer + # to already defined associations. When using named associations count returns the number DISTINCT items for the model you're counting. + # See eager loading under Associations. + # * :order: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). + # * :group: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. + # * :select: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not + # include the joined columns. + # * :distinct: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ... + # + # Examples for counting all: + # Person.count # returns the total count of all people + # + # Examples for count by +conditions+ and +joins+ (for backwards compatibility): + # Person.count("age > 26") # returns the number of people older than 26 + # Person.find("age > 26 AND job.salary > 60000", "LEFT JOIN jobs on jobs.person_id = person.id") # returns the total number of rows matching the conditions and joins fetched by SELECT COUNT(*). + # + # Examples for count with options: + # Person.count(:conditions => "age > 26") + # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN. + # Person.count(:conditions => "age > 26 AND job.salary > 60000", :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # finds the number of rows matching the conditions and joins. + # Person.count('id', :conditions => "age > 26") # Performs a COUNT(id) + # Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*') + # + # Note: Person.count(:all) will not work because it will use :all as the condition. Use Person.count instead. + def count(*args) + options = {} + column_name = :all + # For backwards compatibility, we need to handle both count(conditions=nil, joins=nil) or count(options={}) or count(column_name=:all, options={}). + if args.size >= 0 && args.size <= 2 + if args.first.is_a?(Hash) + options = args.first + elsif args[1].is_a?(Hash) + options = args[1] + column_name = args.first + else + # Handle legacy paramter options: def count(conditions=nil, joins=nil) + options.merge!(:conditions => args[0]) if args.length > 0 + options.merge!(:joins => args[1]) if args.length > 1 + end + else + raise(ArgumentError, "Unexpected parameters passed to count(*args): expected either count(conditions=nil, joins=nil) or count(options={})") + end + + if options[:include] || scope(:find, :include) + count_with_associations(options) + else + calculate(:count, column_name, options) + end + end + + # Calculates average value on a given column. The value is returned as a float. See #calculate for examples with options. + # + # Person.average('age') + def average(column_name, options = {}) + calculate(:avg, column_name, options) + end + + # Calculates the minimum value on a given column. The value is returned with the same data type of the column.. See #calculate for examples with options. + # + # Person.minimum('age') + def minimum(column_name, options = {}) + calculate(:min, column_name, options) + end + + # Calculates the maximum value on a given column. The value is returned with the same data type of the column.. See #calculate for examples with options. + # + # Person.maximum('age') + def maximum(column_name, options = {}) + calculate(:max, column_name, options) + end + + # Calculates the sum value on a given column. The value is returned with the same data type of the column.. See #calculate for examples with options. + # + # Person.sum('age') + def sum(column_name, options = {}) + calculate(:sum, column_name, options) + end + + # This calculates aggregate values in the given column: Methods for count, sum, average, minimum, and maximum have been added as shortcuts. + # Options such as :conditions, :order, :group, :having, and :joins can be passed to customize the query. + # + # There are two basic forms of output: + # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float for AVG, and the given column's type for everything else. + # * Grouped values: This returns an ordered hash of the values and groups them by the :group option. It takes either a column name, or the name + # of a belongs_to association. + # + # values = Person.maximum(:age, :group => 'last_name') + # puts values["Drake"] + # => 43 + # + # drake = Family.find_by_last_name('Drake') + # values = Person.maximum(:age, :group => :family) # Person belongs_to :family + # puts values[drake] + # => 43 + # + # values.each do |family, max_age| + # ... + # end + # + # Options: + # * :conditions: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro. + # * :joins: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). + # The records will be returned read-only since they will have attributes that do not correspond to the table's columns. + # * :order: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). + # * :group: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. + # * :select: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not + # include the joined columns. + # * :distinct: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ... + # + # Examples: + # Person.calculate(:count, :all) # The same as Person.count + # Person.average(:age) # SELECT AVG(age) FROM people... + # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for everyone with a last name other than 'Drake' + # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors + def calculate(operation, column_name, options = {}) + validate_calculation_options(operation, options) + column_name = options[:select] if options[:select] + column_name = '*' if column_name == :all + column = column_for column_name + aggregate = select_aggregate(operation, column_name, options) + aggregate_alias = column_alias_for(operation, column_name) + if options[:group] + execute_grouped_calculation(operation, column_name, column, aggregate, aggregate_alias, options) + else + execute_simple_calculation(operation, column_name, column, aggregate, aggregate_alias, options) + end + end + + protected + def construct_calculation_sql(aggregate, aggregate_alias, options) #:nodoc: + scope = scope(:find) + sql = "SELECT #{aggregate} AS #{aggregate_alias}" + sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group] + sql << " FROM #{table_name} " + add_joins!(sql, options, scope) + add_conditions!(sql, options[:conditions], scope) + sql << " GROUP BY #{options[:group_field]}" if options[:group] + sql << " HAVING #{options[:having]}" if options[:group] && options[:having] + sql << " ORDER BY #{options[:order]}" if options[:order] + add_limit!(sql, options) + sql + end + + def execute_simple_calculation(operation, column_name, column, aggregate, aggregate_alias, options) #:nodoc: + value = connection.select_value(construct_calculation_sql(aggregate, aggregate_alias, options)) + type_cast_calculated_value(value, column, operation) + end + + def execute_grouped_calculation(operation, column_name, column, aggregate, aggregate_alias, options) #:nodoc: + group_attr = options[:group].to_s + association = reflect_on_association(group_attr.to_sym) + associated = association && association.macro == :belongs_to # only count belongs_to associations + group_field = (associated ? "#{options[:group]}_id" : options[:group]).to_s + group_alias = column_alias_for(group_field) + group_column = column_for group_field + sql = construct_calculation_sql(aggregate, aggregate_alias, options.merge(:group_field => group_field, :group_alias => group_alias)) + calculated_data = connection.select_all(sql) + + if association + key_ids = calculated_data.collect { |row| row[group_alias] } + key_records = association.klass.base_class.find(key_ids) + key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) } + end + + calculated_data.inject(OrderedHash.new) do |all, row| + key = associated ? key_records[row[group_alias].to_i] : type_cast_calculated_value(row[group_alias], group_column) + value = row[aggregate_alias] + all << [key, type_cast_calculated_value(value, column, operation)] + end + end + + private + def validate_calculation_options(operation, options = {}) + if operation.to_s == 'count' + options.assert_valid_keys(CALCULATIONS_OPTIONS + [:include]) + else + options.assert_valid_keys(CALCULATIONS_OPTIONS) + end + end + + def select_aggregate(operation, column_name, options) + "#{operation}(#{'DISTINCT ' if options[:distinct]}#{column_name})" + end + + # converts a given key to the value that the database adapter returns as + # + # users.id #=> users_id + # sum(id) #=> sum_id + # count(distinct users.id) #=> count_distinct_users_id + # count(*) #=> count_all + def column_alias_for(*keys) + keys.join(' ').downcase.gsub(/\*/, 'all').gsub(/\W+/, ' ').strip.gsub(/ +/, '_') + end + + def column_for(field) + field_name = field.to_s.split('.').last + columns.detect { |c| c.name.to_s == field_name } + end + + def type_cast_calculated_value(value, column, operation = nil) + operation = operation.to_s.downcase + case operation + when 'count' then value.to_i + when 'avg' then value.to_f + else column ? column.type_cast(value) : value + end + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/callbacks.rb b/vendor/rails/activerecord/lib/active_record/callbacks.rb new file mode 100755 index 00000000..454518d2 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/callbacks.rb @@ -0,0 +1,378 @@ +require 'observer' + +module ActiveRecord + # Callbacks are hooks into the lifecycle of an Active Record object that allows you to trigger logic + # before or after an alteration of the object state. This can be used to make sure that associated and + # dependent objects are deleted when destroy is called (by overwriting before_destroy) or to massage attributes + # before they're validated (by overwriting before_validation). As an example of the callbacks initiated, consider + # the Base#save call: + # + # * (-) save + # * (-) valid? + # * (1) before_validation + # * (2) before_validation_on_create + # * (-) validate + # * (-) validate_on_create + # * (3) after_validation + # * (4) after_validation_on_create + # * (5) before_save + # * (6) before_create + # * (-) create + # * (7) after_create + # * (8) after_save + # + # That's a total of eight callbacks, which gives you immense power to react and prepare for each state in the + # Active Record lifecycle. + # + # Examples: + # class CreditCard < ActiveRecord::Base + # # Strip everything but digits, so the user can specify "555 234 34" or + # # "5552-3434" or both will mean "55523434" + # def before_validation_on_create + # self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number") + # end + # end + # + # class Subscription < ActiveRecord::Base + # before_create :record_signup + # + # private + # def record_signup + # self.signed_up_on = Date.today + # end + # end + # + # class Firm < ActiveRecord::Base + # # Destroys the associated clients and people when the firm is destroyed + # before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" } + # before_destroy { |record| Client.destroy_all "client_of = #{record.id}" } + # end + # + # == Inheritable callback queues + # + # Besides the overwriteable callback methods, it's also possible to register callbacks through the use of the callback macros. + # Their main advantage is that the macros add behavior into a callback queue that is kept intact down through an inheritance + # hierarchy. Example: + # + # class Topic < ActiveRecord::Base + # before_destroy :destroy_author + # end + # + # class Reply < Topic + # before_destroy :destroy_readers + # end + # + # Now, when Topic#destroy is run only +destroy_author+ is called. When Reply#destroy is run both +destroy_author+ and + # +destroy_readers+ is called. Contrast this to the situation where we've implemented the save behavior through overwriteable + # methods: + # + # class Topic < ActiveRecord::Base + # def before_destroy() destroy_author end + # end + # + # class Reply < Topic + # def before_destroy() destroy_readers end + # end + # + # In that case, Reply#destroy would only run +destroy_readers+ and _not_ +destroy_author+. So use the callback macros when + # you want to ensure that a certain callback is called for the entire hierarchy and the regular overwriteable methods when you + # want to leave it up to each descendent to decide whether they want to call +super+ and trigger the inherited callbacks. + # + # *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the callbacks before specifying the + # associations. Otherwise, you might trigger the loading of a child before the parent has registered the callbacks and they won't + # be inherited. + # + # == Types of callbacks + # + # There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects, + # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects are the + # recommended approaches, inline methods using a proc are sometimes appropriate (such as for creating mix-ins), and inline + # eval methods are deprecated. + # + # The method reference callbacks work by specifying a protected or private method available in the object, like this: + # + # class Topic < ActiveRecord::Base + # before_destroy :delete_parents + # + # private + # def delete_parents + # self.class.delete_all "parent_id = #{id}" + # end + # end + # + # The callback objects have methods named after the callback called with the record as the only parameter, such as: + # + # class BankAccount < ActiveRecord::Base + # before_save EncryptionWrapper.new("credit_card_number") + # after_save EncryptionWrapper.new("credit_card_number") + # after_initialize EncryptionWrapper.new("credit_card_number") + # end + # + # class EncryptionWrapper + # def initialize(attribute) + # @attribute = attribute + # end + # + # def before_save(record) + # record.credit_card_number = encrypt(record.credit_card_number) + # end + # + # def after_save(record) + # record.credit_card_number = decrypt(record.credit_card_number) + # end + # + # alias_method :after_find, :after_save + # + # private + # def encrypt(value) + # # Secrecy is committed + # end + # + # def decrypt(value) + # # Secrecy is unveiled + # end + # end + # + # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has + # a method by the name of the callback messaged. + # + # The callback macros usually accept a symbol for the method they're supposed to run, but you can also pass a "method string", + # which will then be evaluated within the binding of the callback. Example: + # + # class Topic < ActiveRecord::Base + # before_destroy 'self.class.delete_all "parent_id = #{id}"' + # end + # + # Notice that single plings (') are used so the #{id} part isn't evaluated until the callback is triggered. Also note that these + # inline callbacks can be stacked just like the regular ones: + # + # class Topic < ActiveRecord::Base + # before_destroy 'self.class.delete_all "parent_id = #{id}"', + # 'puts "Evaluated after parents are destroyed"' + # end + # + # == The after_find and after_initialize exceptions + # + # Because after_find and after_initialize are called for each object found and instantiated by a finder, such as Base.find(:all), we've had + # to implement a simple performance constraint (50% more speed on a simple test case). Unlike all the other callbacks, after_find and + # after_initialize will only be run if an explicit implementation is defined (def after_find). In that case, all of the + # callback types will be called. + # + # == Cancelling callbacks + # + # If a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns + # false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks + # defined as methods on the model, which are called last. + module Callbacks + CALLBACKS = %w( + after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation + after_validation before_validation_on_create after_validation_on_create before_validation_on_update + after_validation_on_update before_destroy after_destroy + ) + + def self.append_features(base) #:nodoc: + super + + base.extend(ClassMethods) + base.class_eval do + class << self + include Observable + alias_method :instantiate_without_callbacks, :instantiate + alias_method :instantiate, :instantiate_with_callbacks + end + + alias_method :initialize_without_callbacks, :initialize + alias_method :initialize, :initialize_with_callbacks + + alias_method :create_or_update_without_callbacks, :create_or_update + alias_method :create_or_update, :create_or_update_with_callbacks + + alias_method :valid_without_callbacks, :valid? + alias_method :valid?, :valid_with_callbacks + + alias_method :create_without_callbacks, :create + alias_method :create, :create_with_callbacks + + alias_method :update_without_callbacks, :update + alias_method :update, :update_with_callbacks + + alias_method :destroy_without_callbacks, :destroy + alias_method :destroy, :destroy_with_callbacks + end + + CALLBACKS.each do |method| + base.class_eval <<-"end_eval" + def self.#{method}(*callbacks, &block) + callbacks << block if block_given? + write_inheritable_array(#{method.to_sym.inspect}, callbacks) + end + end_eval + end + end + + module ClassMethods #:nodoc: + def instantiate_with_callbacks(record) + object = instantiate_without_callbacks(record) + + if object.respond_to_without_attributes?(:after_find) + object.send(:callback, :after_find) + end + + if object.respond_to_without_attributes?(:after_initialize) + object.send(:callback, :after_initialize) + end + + object + end + end + + # Is called when the object was instantiated by one of the finders, like Base.find. + #def after_find() end + + # Is called after the object has been instantiated by a call to Base.new. + #def after_initialize() end + + def initialize_with_callbacks(attributes = nil) #:nodoc: + initialize_without_callbacks(attributes) + result = yield self if block_given? + callback(:after_initialize) if respond_to_without_attributes?(:after_initialize) + result + end + + # Is called _before_ Base.save (regardless of whether it's a create or update save). + def before_save() end + + # Is called _after_ Base.save (regardless of whether it's a create or update save). + # + # class Contact < ActiveRecord::Base + # after_save { logger.info( 'New contact saved!' ) } + # end + def after_save() end + def create_or_update_with_callbacks #:nodoc: + return false if callback(:before_save) == false + result = create_or_update_without_callbacks + callback(:after_save) + result + end + + # Is called _before_ Base.save on new objects that haven't been saved yet (no record exists). + def before_create() end + + # Is called _after_ Base.save on new objects that haven't been saved yet (no record exists). + def after_create() end + def create_with_callbacks #:nodoc: + return false if callback(:before_create) == false + result = create_without_callbacks + callback(:after_create) + result + end + + # Is called _before_ Base.save on existing objects that have a record. + def before_update() end + + # Is called _after_ Base.save on existing objects that have a record. + def after_update() end + + def update_with_callbacks #:nodoc: + return false if callback(:before_update) == false + result = update_without_callbacks + callback(:after_update) + result + end + + # Is called _before_ Validations.validate (which is part of the Base.save call). + def before_validation() end + + # Is called _after_ Validations.validate (which is part of the Base.save call). + def after_validation() end + + # Is called _before_ Validations.validate (which is part of the Base.save call) on new objects + # that haven't been saved yet (no record exists). + def before_validation_on_create() end + + # Is called _after_ Validations.validate (which is part of the Base.save call) on new objects + # that haven't been saved yet (no record exists). + def after_validation_on_create() end + + # Is called _before_ Validations.validate (which is part of the Base.save call) on + # existing objects that have a record. + def before_validation_on_update() end + + # Is called _after_ Validations.validate (which is part of the Base.save call) on + # existing objects that have a record. + def after_validation_on_update() end + + def valid_with_callbacks #:nodoc: + return false if callback(:before_validation) == false + if new_record? then result = callback(:before_validation_on_create) else result = callback(:before_validation_on_update) end + return false if result == false + + result = valid_without_callbacks + + callback(:after_validation) + if new_record? then callback(:after_validation_on_create) else callback(:after_validation_on_update) end + + return result + end + + # Is called _before_ Base.destroy. + # + # Note: If you need to _destroy_ or _nullify_ associated records first, + # use the _:dependent_ option on your associations. + def before_destroy() end + + # Is called _after_ Base.destroy (and all the attributes have been frozen). + # + # class Contact < ActiveRecord::Base + # after_destroy { |record| logger.info( "Contact #{record.id} was destroyed." ) } + # end + def after_destroy() end + def destroy_with_callbacks #:nodoc: + return false if callback(:before_destroy) == false + result = destroy_without_callbacks + callback(:after_destroy) + result + end + + private + def callback(method) + notify(method) + + callbacks_for(method).each do |callback| + result = case callback + when Symbol + self.send(callback) + when String + eval(callback, binding) + when Proc, Method + callback.call(self) + else + if callback.respond_to?(method) + callback.send(method, self) + else + raise ActiveRecordError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method." + end + end + return false if result == false + end + + result = send(method) if respond_to_without_attributes?(method) + + return result + end + + def callbacks_for(method) + self.class.read_inheritable_attribute(method.to_sym) or [] + end + + def invoke_and_notify(method) + notify(method) + send(method) if respond_to_without_attributes?(method) + end + + def notify(method) #:nodoc: + self.class.changed + self.class.notify_observers(method, self) + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb new file mode 100644 index 00000000..2cdc8af6 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -0,0 +1,268 @@ +require 'set' + +module ActiveRecord + class Base + class ConnectionSpecification #:nodoc: + attr_reader :config, :adapter_method + def initialize (config, adapter_method) + @config, @adapter_method = config, adapter_method + end + end + + # Check for activity after at least +verification_timeout+ seconds. + # Defaults to 0 (always check.) + cattr_accessor :verification_timeout + @@verification_timeout = 0 + + # The class -> [adapter_method, config] map + @@defined_connections = {} + + # The class -> thread id -> adapter cache. (class -> adapter if not allow_concurrency) + @@active_connections = {} + + class << self + # Retrieve the connection cache. + def thread_safe_active_connections #:nodoc: + @@active_connections[Thread.current.object_id] ||= {} + end + + def single_threaded_active_connections #:nodoc: + @@active_connections + end + + # pick up the right active_connection method from @@allow_concurrency + if @@allow_concurrency + alias_method :active_connections, :thread_safe_active_connections + else + alias_method :active_connections, :single_threaded_active_connections + end + + # set concurrency support flag (not thread safe, like most of the methods in this file) + def allow_concurrency=(threaded) #:nodoc: + logger.debug "allow_concurrency=#{threaded}" if logger + return if @@allow_concurrency == threaded + clear_all_cached_connections! + @@allow_concurrency = threaded + method_prefix = threaded ? "thread_safe" : "single_threaded" + sing = (class << self; self; end) + [:active_connections, :scoped_methods].each do |method| + sing.send(:alias_method, method, "#{method_prefix}_#{method}") + end + log_connections if logger + end + + def active_connection_name #:nodoc: + @active_connection_name ||= + if active_connections[name] || @@defined_connections[name] + name + elsif self == ActiveRecord::Base + nil + else + superclass.active_connection_name + end + end + + def clear_active_connection_name #:nodoc: + @active_connection_name = nil + subclasses.each { |klass| klass.clear_active_connection_name } + end + + # Returns the connection currently associated with the class. This can + # also be used to "borrow" the connection to do database work unrelated + # to any of the specific Active Records. + def connection + if @active_connection_name && (conn = active_connections[@active_connection_name]) + conn + else + # retrieve_connection sets the cache key. + conn = retrieve_connection + active_connections[@active_connection_name] = conn + end + end + + # Clears the cache which maps classes to connections. + def clear_active_connections! + clear_cache!(@@active_connections) do |name, conn| + conn.disconnect! + end + end + + # Verify active connections. + def verify_active_connections! #:nodoc: + if @@allow_concurrency + remove_stale_cached_threads!(@@active_connections) do |name, conn| + conn.disconnect! + end + end + + active_connections.each_value do |connection| + connection.verify!(@@verification_timeout) + end + end + + private + def clear_cache!(cache, thread_id = nil, &block) + if cache + if @@allow_concurrency + thread_id ||= Thread.current.object_id + thread_cache, cache = cache, cache[thread_id] + return unless cache + end + + cache.each(&block) if block_given? + cache.clear + end + ensure + if thread_cache && @@allow_concurrency + thread_cache.delete(thread_id) + end + end + + # Remove stale threads from the cache. + def remove_stale_cached_threads!(cache, &block) + stale = Set.new(cache.keys) + + Thread.list.each do |thread| + stale.delete(thread.object_id) if thread.alive? + end + + stale.each do |thread_id| + clear_cache!(cache, thread_id, &block) + end + end + + def clear_all_cached_connections! + if @@allow_concurrency + @@active_connections.each_value do |connection_hash_for_thread| + connection_hash_for_thread.each_value {|conn| conn.disconnect! } + connection_hash_for_thread.clear + end + else + @@active_connections.each_value {|conn| conn.disconnect! } + end + @@active_connections.clear + end + end + + # Returns the connection currently associated with the class. This can + # also be used to "borrow" the connection to do database work that isn't + # easily done without going straight to SQL. + def connection + self.class.connection + end + + # Establishes the connection to the database. Accepts a hash as input where + # the :adapter key must be specified with the name of a database adapter (in lower-case) + # example for regular databases (MySQL, Postgresql, etc): + # + # ActiveRecord::Base.establish_connection( + # :adapter => "mysql", + # :host => "localhost", + # :username => "myuser", + # :password => "mypass", + # :database => "somedatabase" + # ) + # + # Example for SQLite database: + # + # ActiveRecord::Base.establish_connection( + # :adapter => "sqlite", + # :database => "path/to/dbfile" + # ) + # + # Also accepts keys as strings (for parsing from yaml for example): + # ActiveRecord::Base.establish_connection( + # "adapter" => "sqlite", + # "database" => "path/to/dbfile" + # ) + # + # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError + # may be returned on an error. + def self.establish_connection(spec = nil) + case spec + when nil + raise AdapterNotSpecified unless defined? RAILS_ENV + establish_connection(RAILS_ENV) + when ConnectionSpecification + clear_active_connection_name + @active_connection_name = name + @@defined_connections[name] = spec + when Symbol, String + if configuration = configurations[spec.to_s] + establish_connection(configuration) + else + raise AdapterNotSpecified, "#{spec} database is not configured" + end + else + spec = spec.symbolize_keys + unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end + adapter_method = "#{spec[:adapter]}_connection" + unless respond_to?(adapter_method) then raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end + remove_connection + establish_connection(ConnectionSpecification.new(spec, adapter_method)) + end + end + + # Locate the connection of the nearest super class. This can be an + # active or defined connections: if it is the latter, it will be + # opened and set as the active connection for the class it was defined + # for (not necessarily the current class). + def self.retrieve_connection #:nodoc: + # Name is nil if establish_connection hasn't been called for + # some class along the inheritance chain up to AR::Base yet. + if name = active_connection_name + if conn = active_connections[name] + # Verify the connection. + conn.verify!(@@verification_timeout) + elsif spec = @@defined_connections[name] + # Activate this connection specification. + klass = name.constantize + klass.connection = spec + conn = active_connections[name] + end + end + + conn or raise ConnectionNotEstablished + end + + # Returns true if a connection that's accessible to this class have already been opened. + def self.connected? + active_connections[active_connection_name] ? true : false + end + + # Remove the connection for this class. This will close the active + # connection and the defined connection (if they exist). The result + # can be used as argument for establish_connection, for easy + # re-establishing of the connection. + def self.remove_connection(klass=self) + spec = @@defined_connections[klass.name] + konn = active_connections[klass.name] + @@defined_connections.delete_if { |key, value| value == spec } + active_connections.delete_if { |key, value| value == konn } + konn.disconnect! if konn + spec.config if spec + end + + # Set the connection for the class. + def self.connection=(spec) #:nodoc: + if spec.kind_of?(ActiveRecord::ConnectionAdapters::AbstractAdapter) + active_connections[name] = spec + elsif spec.kind_of?(ConnectionSpecification) + self.connection = self.send(spec.adapter_method, spec.config) + elsif spec.nil? + raise ConnectionNotEstablished + else + establish_connection spec + end + end + + # connection state logging + def self.log_connections #:nodoc: + if logger + logger.info "Defined connections: #{@@defined_connections.inspect}" + logger.info "Active connections: #{active_connections.inspect}" + logger.info "Active connection name: #{@active_connection_name}" + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb new file mode 100644 index 00000000..c45454ea --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -0,0 +1,104 @@ +module ActiveRecord + module ConnectionAdapters # :nodoc: + module DatabaseStatements + # Returns an array of record hashes with the column names as keys and + # column values as values. + def select_all(sql, name = nil) + end + + # Returns a record hash with the column names as keys and column values + # as values. + def select_one(sql, name = nil) + end + + # Returns a single value from a record + def select_value(sql, name = nil) + result = select_one(sql, name) + result.nil? ? nil : result.values.first + end + + # Returns an array of the values of the first column in a select: + # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] + def select_values(sql, name = nil) + result = select_all(sql, name) + result.map{ |v| v.values.first } + end + + # Executes the SQL statement in the context of this connection. + # This abstract method raises a NotImplementedError. + def execute(sql, name = nil) + raise NotImplementedError, "execute is an abstract method" + end + + # Returns the last auto-generated ID from the affected table. + def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) end + + # Executes the update statement and returns the number of rows affected. + def update(sql, name = nil) end + + # Executes the delete statement and returns the number of rows affected. + def delete(sql, name = nil) end + + # Wrap a block in a transaction. Returns result of block. + def transaction(start_db_transaction = true) + transaction_open = false + begin + if block_given? + if start_db_transaction + begin_db_transaction + transaction_open = true + end + yield + end + rescue Exception => database_transaction_rollback + if transaction_open + transaction_open = false + rollback_db_transaction + end + raise + end + ensure + commit_db_transaction if transaction_open + end + + # Begins the transaction (and turns off auto-committing). + def begin_db_transaction() end + + # Commits the transaction (and turns on auto-committing). + def commit_db_transaction() end + + # Rolls back the transaction (and turns on auto-committing). Must be + # done if the transaction block raises an exception or returns false. + def rollback_db_transaction() end + + # Alias for #add_limit_offset!. + def add_limit!(sql, options) + add_limit_offset!(sql, options) if options + end + + # Appends +LIMIT+ and +OFFSET+ options to a SQL statement. + # This method *modifies* the +sql+ parameter. + # ===== Examples + # add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50}) + # generates + # SELECT * FROM suppliers LIMIT 10 OFFSET 50 + def add_limit_offset!(sql, options) + if limit = options[:limit] + sql << " LIMIT #{limit}" + if offset = options[:offset] + sql << " OFFSET #{offset}" + end + end + end + + def default_sequence_name(table, column) + nil + end + + # Set the sequence to the max value of the table's column. + def reset_sequence!(table, column, sequence = nil) + # Do nothing by default. Implement for PostgreSQL, Oracle, ... + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb new file mode 100644 index 00000000..8d8d085b --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -0,0 +1,51 @@ +module ActiveRecord + module ConnectionAdapters # :nodoc: + module Quoting + # Quotes the column value to help prevent + # {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection]. + def quote(value, column = nil) + case value + when String + if column && column.type == :binary && column.class.respond_to?(:string_to_binary) + "'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode) + elsif column && [:integer, :float].include?(column.type) + value.to_s + else + "'#{quote_string(value)}'" # ' (for ruby-mode) + end + when NilClass then "NULL" + when TrueClass then (column && column.type == :integer ? '1' : quoted_true) + when FalseClass then (column && column.type == :integer ? '0' : quoted_false) + when Float, Fixnum, Bignum then value.to_s + when Date then "'#{value.to_s}'" + when Time, DateTime then "'#{quoted_date(value)}'" + else "'#{quote_string(value.to_yaml)}'" + end + end + + # Quotes a string, escaping any ' (single quote) and \ (backslash) + # characters. + def quote_string(s) + s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode) + end + + # Returns a quoted form of the column name. This is highly adapter + # specific. + def quote_column_name(name) + name + end + + def quoted_true + "'t'" + end + + def quoted_false + "'f'" + end + + def quoted_date(value) + value.strftime("%Y-%m-%d %H:%M:%S") + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb new file mode 100644 index 00000000..16a41446 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -0,0 +1,259 @@ +require 'parsedate' + +module ActiveRecord + module ConnectionAdapters #:nodoc: + # An abstract definition of a column in a table. + class Column + attr_reader :name, :default, :type, :limit, :null, :sql_type + attr_accessor :primary + + # Instantiates a new column in the table. + # + # +name+ is the column's name, as in supplier_id int(11). + # +default+ is the type-casted default value, such as sales_stage varchar(20) default 'new'. + # +sql_type+ is only used to extract the column's length, if necessary. For example, company_name varchar(60). + # +null+ determines if this column allows +NULL+ values. + def initialize(name, default, sql_type = nil, null = true) + @name, @type, @null = name, simplified_type(sql_type), null + @sql_type = sql_type + # have to do this one separately because type_cast depends on #type + @default = type_cast(default) + @limit = extract_limit(sql_type) unless sql_type.nil? + @primary = nil + @text = [:string, :text].include? @type + @number = [:float, :integer].include? @type + end + + def text? + @text + end + + def number? + @number + end + + # Returns the Ruby class that corresponds to the abstract data type. + def klass + case type + when :integer then Fixnum + when :float then Float + when :datetime then Time + when :date then Date + when :timestamp then Time + when :time then Time + when :text, :string then String + when :binary then String + when :boolean then Object + end + end + + # Casts value (which is a String) to an appropriate instance. + def type_cast(value) + return nil if value.nil? + case type + when :string then value + when :text then value + when :integer then value.to_i rescue value ? 1 : 0 + when :float then value.to_f + when :datetime then self.class.string_to_time(value) + when :timestamp then self.class.string_to_time(value) + when :time then self.class.string_to_dummy_time(value) + when :date then self.class.string_to_date(value) + when :binary then self.class.binary_to_string(value) + when :boolean then self.class.value_to_boolean(value) + else value + end + end + + def type_cast_code(var_name) + case type + when :string then nil + when :text then nil + when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" + when :float then "#{var_name}.to_f" + when :datetime then "#{self.class.name}.string_to_time(#{var_name})" + when :timestamp then "#{self.class.name}.string_to_time(#{var_name})" + when :time then "#{self.class.name}.string_to_dummy_time(#{var_name})" + when :date then "#{self.class.name}.string_to_date(#{var_name})" + when :binary then "#{self.class.name}.binary_to_string(#{var_name})" + when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})" + else nil + end + end + + # Returns the human name of the column name. + # + # ===== Examples + # Column.new('sales_stage', ...).human_name #=> 'Sales stage' + def human_name + Base.human_attribute_name(@name) + end + + # Used to convert from Strings to BLOBs + def self.string_to_binary(value) + value + end + + # Used to convert from BLOBs to Strings + def self.binary_to_string(value) + value + end + + def self.string_to_date(string) + return string unless string.is_a?(String) + date_array = ParseDate.parsedate(string) + # treat 0000-00-00 as nil + Date.new(date_array[0], date_array[1], date_array[2]) rescue nil + end + + def self.string_to_time(string) + return string unless string.is_a?(String) + time_array = ParseDate.parsedate(string)[0..5] + # treat 0000-00-00 00:00:00 as nil + Time.send(Base.default_timezone, *time_array) rescue nil + end + + def self.string_to_dummy_time(string) + return string unless string.is_a?(String) + time_array = ParseDate.parsedate(string) + # pad the resulting array with dummy date information + time_array[0] = 2000; time_array[1] = 1; time_array[2] = 1; + Time.send(Base.default_timezone, *time_array) rescue nil + end + + # convert something to a boolean + def self.value_to_boolean(value) + return value if value==true || value==false + case value.to_s.downcase + when "true", "t", "1" then true + else false + end + end + + private + def extract_limit(sql_type) + $1.to_i if sql_type =~ /\((.*)\)/ + end + + def simplified_type(field_type) + case field_type + when /int/i + :integer + when /float|double|decimal|numeric/i + :float + when /datetime/i + :datetime + when /timestamp/i + :timestamp + when /time/i + :time + when /date/i + :date + when /clob/i, /text/i + :text + when /blob/i, /binary/i + :binary + when /char/i, /string/i + :string + when /boolean/i + :boolean + end + end + end + + class IndexDefinition < Struct.new(:table, :name, :unique, :columns) #:nodoc: + end + + class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :default, :null) #:nodoc: + def to_sql + column_sql = "#{base.quote_column_name(name)} #{type_to_sql(type.to_sym, limit)}" + add_column_options!(column_sql, :null => null, :default => default) + column_sql + end + alias to_s :to_sql + + private + def type_to_sql(name, limit) + base.type_to_sql(name, limit) rescue name + end + + def add_column_options!(sql, options) + base.add_column_options!(sql, options.merge(:column => self)) + end + end + + # Represents a SQL table in an abstract way. + # Columns are stored as ColumnDefinition in the #columns attribute. + class TableDefinition + attr_accessor :columns + + def initialize(base) + @columns = [] + @base = base + end + + # Appends a primary key definition to the table definition. + # Can be called multiple times, but this is probably not a good idea. + def primary_key(name) + column(name, native[:primary_key]) + end + + # Returns a ColumnDefinition for the column with name +name+. + def [](name) + @columns.find {|column| column.name.to_s == name.to_s} + end + + # Instantiates a new column for the table. + # The +type+ parameter must be one of the following values: + # :primary_key, :string, :text, + # :integer, :float, :datetime, + # :timestamp, :time, :date, + # :binary, :boolean. + # + # Available options are (none of these exists by default): + # * :limit: + # Requests a maximum column length (:string, :text, + # :binary or :integer columns only) + # * :default: + # The column's default value. You cannot explicitely set the default + # value to +NULL+. Simply leave off this option if you want a +NULL+ + # default value. + # * :null: + # Allows or disallows +NULL+ values in the column. This option could + # have been named :null_allowed. + # + # This method returns self. + # + # ===== Examples + # # Assuming def is an instance of TableDefinition + # def.column(:granted, :boolean) + # #=> granted BOOLEAN + # + # def.column(:picture, :binary, :limit => 2.megabytes) + # #=> picture BLOB(2097152) + # + # def.column(:sales_stage, :string, :limit => 20, :default => 'new', :null => false) + # #=> sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL + def column(name, type, options = {}) + column = self[name] || ColumnDefinition.new(@base, name, type) + column.limit = options[:limit] || native[type.to_sym][:limit] if options[:limit] or native[type.to_sym] + column.default = options[:default] + column.null = options[:null] + @columns << column unless @columns.include? column + self + end + + # Returns a String whose contents are the column definitions + # concatenated together. This string can then be pre and appended to + # to generate the final SQL to create the table. + def to_sql + @columns * ', ' + end + + private + def native + @base.native_database_types + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb new file mode 100644 index 00000000..b57f2c86 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -0,0 +1,271 @@ +module ActiveRecord + module ConnectionAdapters # :nodoc: + module SchemaStatements + # Returns a Hash of mappings from the abstract data types to the native + # database types. See TableDefinition#column for details on the recognized + # abstract data types. + def native_database_types + {} + end + + # This is the maximum length a table alias can be + def table_alias_length + 255 + end + + # Truncates a table alias according to the limits of the current adapter. + def table_alias_for(table_name) + table_name[0..table_alias_length-1].gsub(/\./, '_') + end + + # def tables(name = nil) end + + # Returns an array of indexes for the given table. + # def indexes(table_name, name = nil) end + + # Returns an array of Column objects for the table specified by +table_name+. + # See the concrete implementation for details on the expected parameter values. + def columns(table_name, name = nil) end + + # Creates a new table + # There are two ways to work with #create_table. You can use the block + # form or the regular form, like this: + # + # === Block form + # # create_table() yields a TableDefinition instance + # create_table(:suppliers) do |t| + # t.column :name, :string, :limit => 60 + # # Other fields here + # end + # + # === Regular form + # create_table(:suppliers) + # add_column(:suppliers, :name, :string, {:limit => 60}) + # + # The +options+ hash can include the following keys: + # [:id] + # Set to true or false to add/not add a primary key column + # automatically. Defaults to true. + # [:primary_key] + # The name of the primary key, if one is to be added automatically. + # Defaults to +id+. + # [:options] + # Any extra options you want appended to the table definition. + # [:temporary] + # Make a temporary table. + # [:force] + # Set to true or false to drop the table before creating it. + # Defaults to false. + # + # ===== Examples + # ====== Add a backend specific option to the generated SQL (MySQL) + # create_table(:suppliers, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # generates: + # CREATE TABLE suppliers ( + # id int(11) DEFAULT NULL auto_increment PRIMARY KEY + # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + # + # ====== Rename the primary key column + # create_table(:objects, :primary_key => 'guid') do |t| + # t.column :name, :string, :limit => 80 + # end + # generates: + # CREATE TABLE objects ( + # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY, + # name varchar(80) + # ) + # + # ====== Do not add a primary key column + # create_table(:categories_suppliers, :id => false) do |t| + # t.column :category_id, :integer + # t.column :supplier_id, :integer + # end + # generates: + # CREATE TABLE categories_suppliers_join ( + # category_id int, + # supplier_id int + # ) + # + # See also TableDefinition#column for details on how to create columns. + def create_table(name, options = {}) + table_definition = TableDefinition.new(self) + table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false + + yield table_definition + + if options[:force] + drop_table(name) rescue nil + end + + create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE " + create_sql << "#{name} (" + create_sql << table_definition.to_sql + create_sql << ") #{options[:options]}" + execute create_sql + end + + # Renames a table. + # ===== Example + # rename_table('octopuses', 'octopi') + def rename_table(name, new_name) + raise NotImplementedError, "rename_table is not implemented" + end + + # Drops a table from the database. + def drop_table(name) + execute "DROP TABLE #{name}" + end + + # Adds a new column to the named table. + # See TableDefinition#column for details of the options you can use. + def add_column(table_name, column_name, type, options = {}) + add_column_sql = "ALTER TABLE #{table_name} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit])}" + add_column_options!(add_column_sql, options) + execute(add_column_sql) + end + + # Removes the column from the table definition. + # ===== Examples + # remove_column(:suppliers, :qualification) + def remove_column(table_name, column_name) + execute "ALTER TABLE #{table_name} DROP #{quote_column_name(column_name)}" + end + + # Changes the column's definition according to the new options. + # See TableDefinition#column for details of the options you can use. + # ===== Examples + # change_column(:suppliers, :name, :string, :limit => 80) + # change_column(:accounts, :description, :text) + def change_column(table_name, column_name, type, options = {}) + raise NotImplementedError, "change_column is not implemented" + end + + # Sets a new default value for a column. If you want to set the default + # value to +NULL+, you are out of luck. You need to + # DatabaseStatements#execute the apppropriate SQL statement yourself. + # ===== Examples + # change_column_default(:suppliers, :qualification, 'new') + # change_column_default(:accounts, :authorized, 1) + def change_column_default(table_name, column_name, default) + raise NotImplementedError, "change_column_default is not implemented" + end + + # Renames a column. + # ===== Example + # rename_column(:suppliers, :description, :name) + def rename_column(table_name, column_name, new_column_name) + raise NotImplementedError, "rename_column is not implemented" + end + + # Adds a new index to the table. +column_name+ can be a single Symbol, or + # an Array of Symbols. + # + # The index will be named after the table and the first column names, + # unless you pass +:name+ as an option. + # + # When creating an index on multiple columns, the first column is used as a name + # for the index. For example, when you specify an index on two columns + # [+:first+, +:last+], the DBMS creates an index for both columns as well as an + # index for the first colum +:first+. Using just the first name for this index + # makes sense, because you will never have to create a singular index with this + # name. + # + # ===== Examples + # ====== Creating a simple index + # add_index(:suppliers, :name) + # generates + # CREATE INDEX suppliers_name_index ON suppliers(name) + # ====== Creating a unique index + # add_index(:accounts, [:branch_id, :party_id], :unique => true) + # generates + # CREATE UNIQUE INDEX accounts_branch_id_index ON accounts(branch_id, party_id) + # ====== Creating a named index + # add_index(:accounts, [:branch_id, :party_id], :unique => true, :name => 'by_branch_party') + # generates + # CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id) + def add_index(table_name, column_name, options = {}) + column_names = Array(column_name) + index_name = index_name(table_name, :column => column_names.first) + + if Hash === options # legacy support, since this param was a string + index_type = options[:unique] ? "UNIQUE" : "" + index_name = options[:name] || index_name + else + index_type = options + end + quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ") + execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{table_name} (#{quoted_column_names})" + end + + # Remove the given index from the table. + # + # Remove the suppliers_name_index in the suppliers table (legacy support, use the second or third forms). + # remove_index :suppliers, :name + # Remove the index named accounts_branch_id in the accounts table. + # remove_index :accounts, :column => :branch_id + # Remove the index named by_branch_party in the accounts table. + # remove_index :accounts, :name => :by_branch_party + # + # You can remove an index on multiple columns by specifying the first column. + # add_index :accounts, [:username, :password] + # remove_index :accounts, :username + def remove_index(table_name, options = {}) + execute "DROP INDEX #{quote_column_name(index_name(table_name, options))} ON #{table_name}" + end + + def index_name(table_name, options) #:nodoc: + if Hash === options # legacy support + if options[:column] + "#{table_name}_#{options[:column]}_index" + elsif options[:name] + options[:name] + else + raise ArgumentError, "You must specify the index name" + end + else + "#{table_name}_#{options}_index" + end + end + + # Returns a string of CREATE TABLE SQL statement(s) for recreating the + # entire structure of the database. + def structure_dump + end + + # Should not be called normally, but this operation is non-destructive. + # The migrations module handles this automatically. + def initialize_schema_information + begin + execute "CREATE TABLE #{ActiveRecord::Migrator.schema_info_table_name} (version #{type_to_sql(:integer)})" + execute "INSERT INTO #{ActiveRecord::Migrator.schema_info_table_name} (version) VALUES(0)" + rescue ActiveRecord::StatementInvalid + # Schema has been intialized + end + end + + def dump_schema_information #:nodoc: + begin + if (current_schema = ActiveRecord::Migrator.current_version) > 0 + return "INSERT INTO #{ActiveRecord::Migrator.schema_info_table_name} (version) VALUES (#{current_schema})" + end + rescue ActiveRecord::StatementInvalid + # No Schema Info + end + end + + + def type_to_sql(type, limit = nil) #:nodoc: + native = native_database_types[type] + limit ||= native[:limit] + column_type_sql = native[:name] + column_type_sql << "(#{limit})" if limit + column_type_sql + end + + def add_column_options!(sql, options) #:nodoc: + sql << " DEFAULT #{quote(options[:default], options[:column])}" unless options[:default].nil? + sql << " NOT NULL" if options[:null] == false + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb new file mode 100755 index 00000000..4eea7e54 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -0,0 +1,153 @@ +require 'benchmark' +require 'date' + +require 'active_record/connection_adapters/abstract/schema_definitions' +require 'active_record/connection_adapters/abstract/schema_statements' +require 'active_record/connection_adapters/abstract/database_statements' +require 'active_record/connection_adapters/abstract/quoting' +require 'active_record/connection_adapters/abstract/connection_specification' + +module ActiveRecord + module ConnectionAdapters # :nodoc: + # All the concrete database adapters follow the interface laid down in this class. + # You can use this interface directly by borrowing the database connection from the Base with + # Base.connection. + # + # Most of the methods in the adapter are useful during migrations. Most + # notably, SchemaStatements#create_table, SchemaStatements#drop_table, + # SchemaStatements#add_index, SchemaStatements#remove_index, + # SchemaStatements#add_column, SchemaStatements#change_column and + # SchemaStatements#remove_column are very useful. + class AbstractAdapter + include Quoting, DatabaseStatements, SchemaStatements + @@row_even = true + + def initialize(connection, logger = nil) #:nodoc: + @connection, @logger = connection, logger + @runtime = 0 + @last_verification = 0 + end + + # Returns the human-readable name of the adapter. Use mixed case - one + # can always use downcase if needed. + def adapter_name + 'Abstract' + end + + # Does this adapter support migrations? Backend specific, as the + # abstract adapter always returns +false+. + def supports_migrations? + false + end + + # Does this adapter support using DISTINCT within COUNT? This is +true+ + # for all adapters except sqlite. + def supports_count_distinct? + true + end + + # Should primary key values be selected from their corresponding + # sequence before the insert statement? If true, next_sequence_value + # is called before each insert to set the record's primary key. + # This is false for all adapters but Firebird. + def prefetch_primary_key?(table_name = nil) + false + end + + def reset_runtime #:nodoc: + rt, @runtime = @runtime, 0 + rt + end + + + # CONNECTION MANAGEMENT ==================================== + + # Is this connection active and ready to perform queries? + def active? + @active != false + end + + # Close this connection and open a new one in its place. + def reconnect! + @active = true + end + + # Close this connection + def disconnect! + @active = false + end + + # Lazily verify this connection, calling +active?+ only if it hasn't + # been called for +timeout+ seconds. + def verify!(timeout) + now = Time.now.to_i + if (now - @last_verification) > timeout + reconnect! unless active? + @last_verification = now + end + end + + # Provides access to the underlying database connection. Useful for + # when you need to call a proprietary method such as postgresql's lo_* + # methods + def raw_connection + @connection + end + + protected + def log(sql, name) + if block_given? + if @logger and @logger.level <= Logger::INFO + result = nil + seconds = Benchmark.realtime { result = yield } + @runtime += seconds + log_info(sql, name, seconds) + result + else + yield + end + else + log_info(sql, name, 0) + nil + end + rescue Exception => e + # Log message and raise exception. + # Set last_verfication to 0, so that connection gets verified + # upon reentering the request loop + @last_verification = 0 + message = "#{e.class.name}: #{e.message}: #{sql}" + log_info(message, name, 0) + raise ActiveRecord::StatementInvalid, message + end + + def log_info(sql, name, runtime) + return unless @logger + + @logger.debug( + format_log_entry( + "#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})", + sql.gsub(/ +/, " ") + ) + ) + end + + def format_log_entry(message, dump = nil) + if ActiveRecord::Base.colorize_logging + if @@row_even + @@row_even = false + message_color, dump_color = "4;36;1", "0;1" + else + @@row_even = true + message_color, dump_color = "4;35;1", "0" + end + + log_entry = " \e[#{message_color}m#{message}\e[0m " + log_entry << "\e[#{dump_color}m%#{String === dump ? 's' : 'p'}\e[0m" % dump if dump + log_entry + else + "%s %s" % [message, dump] + end + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/db2_adapter.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/db2_adapter.rb new file mode 100644 index 00000000..3b81c526 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/db2_adapter.rb @@ -0,0 +1,238 @@ +# Author/Maintainer: Maik Schmidt + +require 'active_record/connection_adapters/abstract_adapter' + +begin + require 'db2/db2cli' unless self.class.const_defined?(:DB2CLI) + require 'active_record/vendor/db2' + + module ActiveRecord + class Base + # Establishes a connection to the database that's used by + # all Active Record objects + def self.db2_connection(config) # :nodoc: + config = config.symbolize_keys + usr = config[:username] + pwd = config[:password] + schema = config[:schema] + + if config.has_key?(:database) + database = config[:database] + else + raise ArgumentError, 'No database specified. Missing argument: database.' + end + + connection = DB2::Connection.new(DB2::Environment.new) + connection.connect(database, usr, pwd) + ConnectionAdapters::DB2Adapter.new(connection, logger, :schema => schema) + end + end + + module ConnectionAdapters + # The DB2 adapter works with the C-based CLI driver (http://rubyforge.org/projects/ruby-dbi/) + # + # Options: + # + # * :username -- Defaults to nothing + # * :password -- Defaults to nothing + # * :database -- The name of the database. No default, must be provided. + # * :schema -- Database schema to be set initially. + class DB2Adapter < AbstractAdapter + def initialize(connection, logger, connection_options) + super(connection, logger) + @connection_options = connection_options + if schema = @connection_options[:schema] + with_statement do |stmt| + stmt.exec_direct("SET SCHEMA=#{schema}") + end + end + end + + def select_all(sql, name = nil) + select(sql, name) + end + + def select_one(sql, name = nil) + select(sql, name).first + end + + def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + execute(sql, name = nil) + id_value || last_insert_id + end + + def execute(sql, name = nil) + rows_affected = 0 + with_statement do |stmt| + log(sql, name) do + stmt.exec_direct(sql) + rows_affected = stmt.row_count + end + end + rows_affected + end + + alias_method :update, :execute + alias_method :delete, :execute + + def begin_db_transaction + @connection.set_auto_commit_off + end + + def commit_db_transaction + @connection.commit + @connection.set_auto_commit_on + end + + def rollback_db_transaction + @connection.rollback + @connection.set_auto_commit_on + end + + def quote_column_name(column_name) + column_name + end + + def adapter_name() + 'DB2' + end + + def quote_string(string) + string.gsub(/'/, "''") # ' (for ruby-mode) + end + + def add_limit_offset!(sql, options) + if limit = options[:limit] + offset = options[:offset] || 0 + # The following trick was added by andrea+rails@webcom.it. + sql.gsub!(/SELECT/i, 'SELECT B.* FROM (SELECT A.*, row_number() over () AS internal$rownum FROM (SELECT') + sql << ") A ) B WHERE B.internal$rownum > #{offset} AND B.internal$rownum <= #{limit + offset}" + end + end + + def tables(name = nil) + result = [] + schema = @connection_options[:schema] || '%' + with_statement do |stmt| + stmt.tables(schema).each { |t| result << t[2].downcase } + end + result + end + + def indexes(table_name, name = nil) + tmp = {} + schema = @connection_options[:schema] || '' + with_statement do |stmt| + stmt.indexes(table_name, schema).each do |t| + next unless t[5] + next if t[4] == 'SYSIBM' # Skip system indexes. + idx_name = t[5].downcase + col_name = t[8].downcase + if tmp.has_key?(idx_name) + tmp[idx_name].columns << col_name + else + is_unique = t[3] == 0 + tmp[idx_name] = IndexDefinition.new(table_name, idx_name, is_unique, [col_name]) + end + end + end + tmp.values + end + + def columns(table_name, name = nil) + result = [] + schema = @connection_options[:schema] || '%' + with_statement do |stmt| + stmt.columns(table_name, schema).each do |c| + c_name = c[3].downcase + c_default = c[12] == 'NULL' ? nil : c[12] + c_default.gsub!(/^'(.*)'$/, '\1') if !c_default.nil? + c_type = c[5].downcase + c_type += "(#{c[6]})" if !c[6].nil? && c[6] != '' + result << Column.new(c_name, c_default, c_type, c[17] == 'YES') + end + end + result + end + + def native_database_types + { + :primary_key => 'int generated by default as identity (start with 42) primary key', + :string => { :name => 'varchar', :limit => 255 }, + :text => { :name => 'clob', :limit => 32768 }, + :integer => { :name => 'int' }, + :float => { :name => 'float' }, + :datetime => { :name => 'timestamp' }, + :timestamp => { :name => 'timestamp' }, + :time => { :name => 'time' }, + :date => { :name => 'date' }, + :binary => { :name => 'blob', :limit => 32768 }, + :boolean => { :name => 'decimal', :limit => 1 } + } + end + + def quoted_true + '1' + end + + def quoted_false + '0' + end + + def active? + @connection.select_one 'select 1 from ibm.sysdummy1' + true + rescue Exception + false + end + + def reconnect! + end + + def table_alias_length + 128 + end + + private + + def with_statement + stmt = DB2::Statement.new(@connection) + yield stmt + stmt.free + end + + def last_insert_id + row = select_one(<<-GETID.strip) + with temp(id) as (values (identity_val_local())) select * from temp + GETID + row['id'].to_i + end + + def select(sql, name = nil) + rows = [] + with_statement do |stmt| + log(sql, name) do + stmt.exec_direct("#{sql.gsub(/=\s*null/i, 'IS NULL')} with ur") + end + + while row = stmt.fetch_as_hash + row.delete('internal$rownum') + rows << row + end + end + rows + end + end + end + end +rescue LoadError + # DB2 driver is unavailable. + module ActiveRecord # :nodoc: + class Base + def self.db2_connection(config) # :nodoc: + # Set up a reasonable error message + raise LoadError, "DB2 Libraries could not be loaded." + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/firebird_adapter.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/firebird_adapter.rb new file mode 100644 index 00000000..9bf047f0 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/firebird_adapter.rb @@ -0,0 +1,414 @@ +# Author: Ken Kunz + +require 'active_record/connection_adapters/abstract_adapter' + +module FireRuby # :nodoc: all + class Database + def self.new_from_params(database, host, port, service) + db_string = "" + if host + db_string << host + db_string << "/#{service || port}" if service || port + db_string << ":" + end + db_string << database + new(db_string) + end + end +end + +module ActiveRecord + class << Base + def firebird_connection(config) # :nodoc: + require_library_or_gem 'fireruby' + unless defined? FireRuby::SQLType + raise AdapterNotFound, + 'The Firebird adapter requires FireRuby version 0.4.0 or greater; you appear ' << + 'to be running an older version -- please update FireRuby (gem install fireruby).' + end + config = config.symbolize_keys + unless config.has_key?(:database) + raise ArgumentError, "No database specified. Missing argument: database." + end + options = config[:charset] ? { CHARACTER_SET => config[:charset] } : {} + connection_params = [config[:username], config[:password], options] + db = FireRuby::Database.new_from_params(*config.values_at(:database, :host, :port, :service)) + connection = db.connect(*connection_params) + ConnectionAdapters::FirebirdAdapter.new(connection, logger, connection_params) + end + end + + module ConnectionAdapters + class FirebirdColumn < Column # :nodoc: + VARCHAR_MAX_LENGTH = 32_765 + BLOB_MAX_LENGTH = 32_767 + + def initialize(name, domain, type, sub_type, length, precision, scale, default_source, null_flag) + @firebird_type = FireRuby::SQLType.to_base_type(type, sub_type).to_s + super(name.downcase, nil, @firebird_type, !null_flag) + @default = parse_default(default_source) if default_source + @limit = type == 'BLOB' ? BLOB_MAX_LENGTH : length + @domain, @sub_type, @precision, @scale = domain, sub_type, precision, scale + end + + def type + if @domain =~ /BOOLEAN/ + :boolean + elsif @type == :binary and @sub_type == 1 + :text + else + @type + end + end + + # Submits a _CAST_ query to the database, casting the default value to the specified SQL type. + # This enables Firebird to provide an actual value when context variables are used as column + # defaults (such as CURRENT_TIMESTAMP). + def default + if @default + sql = "SELECT CAST(#{@default} AS #{column_def}) FROM RDB$DATABASE" + connection = ActiveRecord::Base.active_connections.values.detect { |conn| conn && conn.adapter_name == 'Firebird' } + if connection + type_cast connection.execute(sql).to_a.first['CAST'] + else + raise ConnectionNotEstablished, "No Firebird connections established." + end + end + end + + def type_cast(value) + if type == :boolean + value == true or value == ActiveRecord::ConnectionAdapters::FirebirdAdapter.boolean_domain[:true] + else + super + end + end + + private + def parse_default(default_source) + default_source =~ /^\s*DEFAULT\s+(.*)\s*$/i + return $1 unless $1.upcase == "NULL" + end + + def column_def + case @firebird_type + when 'BLOB' then "VARCHAR(#{VARCHAR_MAX_LENGTH})" + when 'CHAR', 'VARCHAR' then "#{@firebird_type}(#{@limit})" + when 'NUMERIC', 'DECIMAL' then "#{@firebird_type}(#{@precision},#{@scale.abs})" + when 'DOUBLE' then "DOUBLE PRECISION" + else @firebird_type + end + end + + def simplified_type(field_type) + if field_type == 'TIMESTAMP' + :datetime + else + super + end + end + end + + # The Firebird adapter relies on the FireRuby[http://rubyforge.org/projects/fireruby/] + # extension, version 0.4.0 or later (available as a gem or from + # RubyForge[http://rubyforge.org/projects/fireruby/]). FireRuby works with + # Firebird 1.5.x on Linux, OS X and Win32 platforms. + # + # == Usage Notes + # + # === Sequence (Generator) Names + # The Firebird adapter supports the same approach adopted for the Oracle + # adapter. See ActiveRecord::Base#set_sequence_name for more details. + # + # Note that in general there is no need to create a BEFORE INSERT + # trigger corresponding to a Firebird sequence generator when using + # ActiveRecord. In other words, you don't have to try to make Firebird + # simulate an AUTO_INCREMENT or +IDENTITY+ column. When saving a + # new record, ActiveRecord pre-fetches the next sequence value for the table + # and explicitly includes it in the +INSERT+ statement. (Pre-fetching the + # next primary key value is the only reliable method for the Firebird + # adapter to report back the +id+ after a successful insert.) + # + # === BOOLEAN Domain + # Firebird 1.5 does not provide a native +BOOLEAN+ type. But you can easily + # define a +BOOLEAN+ _domain_ for this purpose, e.g.: + # + # CREATE DOMAIN D_BOOLEAN AS SMALLINT CHECK (VALUE IN (0, 1)); + # + # When the Firebird adapter encounters a column that is based on a domain + # that includes "BOOLEAN" in the domain name, it will attempt to treat + # the column as a +BOOLEAN+. + # + # By default, the Firebird adapter will assume that the BOOLEAN domain is + # defined as above. This can be modified if needed. For example, if you + # have a legacy schema with the following +BOOLEAN+ domain defined: + # + # CREATE DOMAIN BOOLEAN AS CHAR(1) CHECK (VALUE IN ('T', 'F')); + # + # ...you can add the following line to your environment.rb file: + # + # ActiveRecord::ConnectionAdapters::FirebirdAdapter.boolean_domain = { :true => 'T', :false => 'F' } + # + # === BLOB Elements + # The Firebird adapter currently provides only limited support for +BLOB+ + # columns. You cannot currently retrieve or insert a +BLOB+ as an IO stream. + # When selecting a +BLOB+, the entire element is converted into a String. + # When inserting or updating a +BLOB+, the entire value is included in-line + # in the SQL statement, limiting you to values <= 32KB in size. + # + # === Column Name Case Semantics + # Firebird and ActiveRecord have somewhat conflicting case semantics for + # column names. + # + # [*Firebird*] + # The standard practice is to use unquoted column names, which can be + # thought of as case-insensitive. (In fact, Firebird converts them to + # uppercase.) Quoted column names (not typically used) are case-sensitive. + # [*ActiveRecord*] + # Attribute accessors corresponding to column names are case-sensitive. + # The defaults for primary key and inheritance columns are lowercase, and + # in general, people use lowercase attribute names. + # + # In order to map between the differing semantics in a way that conforms + # to common usage for both Firebird and ActiveRecord, uppercase column names + # in Firebird are converted to lowercase attribute names in ActiveRecord, + # and vice-versa. Mixed-case column names retain their case in both + # directions. Lowercase (quoted) Firebird column names are not supported. + # This is similar to the solutions adopted by other adapters. + # + # In general, the best approach is to use unqouted (case-insensitive) column + # names in your Firebird DDL (or if you must quote, use uppercase column + # names). These will correspond to lowercase attributes in ActiveRecord. + # + # For example, a Firebird table based on the following DDL: + # + # CREATE TABLE products ( + # id BIGINT NOT NULL PRIMARY KEY, + # "TYPE" VARCHAR(50), + # name VARCHAR(255) ); + # + # ...will correspond to an ActiveRecord model class called +Product+ with + # the following attributes: +id+, +type+, +name+. + # + # ==== Quoting "TYPE" and other Firebird reserved words: + # In ActiveRecord, the default inheritance column name is +type+. The word + # _type_ is a Firebird reserved word, so it must be quoted in any Firebird + # SQL statements. Because of the case mapping described above, you should + # always reference this column using quoted-uppercase syntax + # ("TYPE") within Firebird DDL or other SQL statements (as in the + # example above). This holds true for any other Firebird reserved words used + # as column names as well. + # + # === Migrations + # The Firebird adapter does not currently support Migrations. I hope to + # add this feature in the near future. + # + # == Connection Options + # The following options are supported by the Firebird adapter. None of the + # options have default values. + # + # :database:: + # Required option. Specifies one of: (i) a Firebird database alias; + # (ii) the full path of a database file; _or_ (iii) a full Firebird + # connection string. Do not specify :host, :service + # or :port as separate options when using a full connection + # string. + # :host:: + # Set to "remote.host.name" for remote database connections. + # May be omitted for local connections if a full database path is + # specified for :database. Some platforms require a value of + # "localhost" for local connections when using a Firebird + # database _alias_. + # :service:: + # Specifies a service name for the connection. Only used if :host + # is provided. Required when connecting to a non-standard service. + # :port:: + # Specifies the connection port. Only used if :host is provided + # and :service is not. Required when connecting to a non-standard + # port and :service is not defined. + # :username:: + # Specifies the database user. May be omitted or set to +nil+ (together + # with :password) to use the underlying operating system user + # credentials on supported platforms. + # :password:: + # Specifies the database password. Must be provided if :username + # is explicitly specified; should be omitted if OS user credentials are + # are being used. + # :charset:: + # Specifies the character set to be used by the connection. Refer to + # Firebird documentation for valid options. + class FirebirdAdapter < AbstractAdapter + @@boolean_domain = { :true => 1, :false => 0 } + cattr_accessor :boolean_domain + + def initialize(connection, logger, connection_params=nil) + super(connection, logger) + @connection_params = connection_params + end + + def adapter_name # :nodoc: + 'Firebird' + end + + # Returns true for Firebird adapter (since Firebird requires primary key + # values to be pre-fetched before insert). See also #next_sequence_value. + def prefetch_primary_key?(table_name = nil) + true + end + + def default_sequence_name(table_name, primary_key) # :nodoc: + "#{table_name}_seq" + end + + + # QUOTING ================================================== + + def quote(value, column = nil) # :nodoc: + if [Time, DateTime].include?(value.class) + "CAST('#{value.strftime("%Y-%m-%d %H:%M:%S")}' AS TIMESTAMP)" + else + super + end + end + + def quote_string(string) # :nodoc: + string.gsub(/'/, "''") + end + + def quote_column_name(column_name) # :nodoc: + %Q("#{ar_to_fb_case(column_name)}") + end + + def quoted_true # :nodoc: + quote(boolean_domain[:true]) + end + + def quoted_false # :nodoc: + quote(boolean_domain[:false]) + end + + + # CONNECTION MANAGEMENT ==================================== + + def active? + not @connection.closed? + end + + def reconnect! + @connection.close + @connection = @connection.database.connect(*@connection_params) + end + + + # DATABASE STATEMENTS ====================================== + + def select_all(sql, name = nil) # :nodoc: + select(sql, name) + end + + def select_one(sql, name = nil) # :nodoc: + result = select(sql, name) + result.nil? ? nil : result.first + end + + def execute(sql, name = nil, &block) # :nodoc: + log(sql, name) do + if @transaction + @connection.execute(sql, @transaction, &block) + else + @connection.execute_immediate(sql, &block) + end + end + end + + def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) # :nodoc: + execute(sql, name) + id_value + end + + alias_method :update, :execute + alias_method :delete, :execute + + def begin_db_transaction() # :nodoc: + @transaction = @connection.start_transaction + end + + def commit_db_transaction() # :nodoc: + @transaction.commit + ensure + @transaction = nil + end + + def rollback_db_transaction() # :nodoc: + @transaction.rollback + ensure + @transaction = nil + end + + def add_limit_offset!(sql, options) # :nodoc: + if options[:limit] + limit_string = "FIRST #{options[:limit]}" + limit_string << " SKIP #{options[:offset]}" if options[:offset] + sql.sub!(/\A(\s*SELECT\s)/i, '\&' + limit_string + ' ') + end + end + + # Returns the next sequence value from a sequence generator. Not generally + # called directly; used by ActiveRecord to get the next primary key value + # when inserting a new database record (see #prefetch_primary_key?). + def next_sequence_value(sequence_name) + FireRuby::Generator.new(sequence_name, @connection).next(1) + end + + + # SCHEMA STATEMENTS ======================================== + + def columns(table_name, name = nil) # :nodoc: + sql = <<-END_SQL + SELECT r.rdb$field_name, r.rdb$field_source, f.rdb$field_type, f.rdb$field_sub_type, + f.rdb$field_length, f.rdb$field_precision, f.rdb$field_scale, + COALESCE(r.rdb$default_source, f.rdb$default_source) rdb$default_source, + COALESCE(r.rdb$null_flag, f.rdb$null_flag) rdb$null_flag + FROM rdb$relation_fields r + JOIN rdb$fields f ON r.rdb$field_source = f.rdb$field_name + WHERE r.rdb$relation_name = '#{table_name.to_s.upcase}' + ORDER BY r.rdb$field_position + END_SQL + execute(sql, name).collect do |field| + field_values = field.values.collect do |value| + case value + when String then value.rstrip + when FireRuby::Blob then value.to_s + else value + end + end + FirebirdColumn.new(*field_values) + end + end + + private + def select(sql, name = nil) + execute(sql, name).collect do |row| + hashed_row = {} + row.each do |column, value| + value = value.to_s if FireRuby::Blob === value + hashed_row[fb_to_ar_case(column)] = value + end + hashed_row + end + end + + # Maps uppercase Firebird column names to lowercase for ActiveRecord; + # mixed-case columns retain their original case. + def fb_to_ar_case(column_name) + column_name =~ /[[:lower:]]/ ? column_name : column_name.downcase + end + + # Maps lowercase ActiveRecord column names to uppercase for Fierbird; + # mixed-case columns retain their original case. + def ar_to_fb_case(column_name) + column_name =~ /[[:upper:]]/ ? column_name : column_name.upcase + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb new file mode 100755 index 00000000..7a697c23 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -0,0 +1,357 @@ +require 'active_record/connection_adapters/abstract_adapter' + +module ActiveRecord + class Base + # Establishes a connection to the database that's used by all Active Record objects. + def self.mysql_connection(config) # :nodoc: + # Only include the MySQL driver if one hasn't already been loaded + unless defined? Mysql + begin + require_library_or_gem 'mysql' + rescue LoadError => cannot_require_mysql + # Only use the supplied backup Ruby/MySQL driver if no driver is already in place + begin + require 'active_record/vendor/mysql' + rescue LoadError + raise cannot_require_mysql + end + end + end + + config = config.symbolize_keys + host = config[:host] + port = config[:port] + socket = config[:socket] + username = config[:username] ? config[:username].to_s : 'root' + password = config[:password].to_s + + if config.has_key?(:database) + database = config[:database] + else + raise ArgumentError, "No database specified. Missing argument: database." + end + + mysql = Mysql.init + mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey] + ConnectionAdapters::MysqlAdapter.new(mysql, logger, [host, username, password, database, port, socket], config) + end + end + + module ConnectionAdapters + class MysqlColumn < Column #:nodoc: + private + def simplified_type(field_type) + return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)") + return :string if field_type =~ /enum/i + super + end + end + + # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with + # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/). + # + # Options: + # + # * :host -- Defaults to localhost + # * :port -- Defaults to 3306 + # * :socket -- Defaults to /tmp/mysql.sock + # * :username -- Defaults to root + # * :password -- Defaults to nothing + # * :database -- The name of the database. No default, must be provided. + # * :sslkey -- Necessary to use MySQL with an SSL connection + # * :sslcert -- Necessary to use MySQL with an SSL connection + # * :sslcapath -- Necessary to use MySQL with an SSL connection + # * :sslcipher -- Necessary to use MySQL with an SSL connection + # + # By default, the MysqlAdapter will consider all columns of type tinyint(1) + # as boolean. If you wish to disable this emulation (which was the default + # behavior in versions 0.13.1 and earlier) you can add the following line + # to your environment.rb file: + # + # ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false + class MysqlAdapter < AbstractAdapter + @@emulate_booleans = true + cattr_accessor :emulate_booleans + + LOST_CONNECTION_ERROR_MESSAGES = [ + "Server shutdown in progress", + "Broken pipe", + "Lost connection to MySQL server during query", + "MySQL server has gone away" + ] + + def initialize(connection, logger, connection_options, config) + super(connection, logger) + @connection_options, @config = connection_options, config + @null_values_in_each_hash = Mysql.const_defined?(:VERSION) + connect + end + + def adapter_name #:nodoc: + 'MySQL' + end + + def supports_migrations? #:nodoc: + true + end + + def native_database_types #:nodoc + { + :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY", + :string => { :name => "varchar", :limit => 255 }, + :text => { :name => "text" }, + :integer => { :name => "int", :limit => 11 }, + :float => { :name => "float" }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "datetime" }, + :time => { :name => "time" }, + :date => { :name => "date" }, + :binary => { :name => "blob" }, + :boolean => { :name => "tinyint", :limit => 1 } + } + end + + + # QUOTING ================================================== + + def quote(value, column = nil) + if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) + s = column.class.string_to_binary(value).unpack("H*")[0] + "x'#{s}'" + else + super + end + end + + def quote_column_name(name) #:nodoc: + "`#{name}`" + end + + def quote_string(string) #:nodoc: + @connection.quote(string) + end + + def quoted_true + "1" + end + + def quoted_false + "0" + end + + + # CONNECTION MANAGEMENT ==================================== + + def active? + if @connection.respond_to?(:stat) + @connection.stat + else + @connection.query 'select 1' + end + + # mysql-ruby doesn't raise an exception when stat fails. + if @connection.respond_to?(:errno) + @connection.errno.zero? + else + true + end + rescue Mysql::Error + false + end + + def reconnect! + disconnect! + connect + end + + def disconnect! + @connection.close rescue nil + end + + + # DATABASE STATEMENTS ====================================== + + def select_all(sql, name = nil) #:nodoc: + select(sql, name) + end + + def select_one(sql, name = nil) #:nodoc: + result = select(sql, name) + result.nil? ? nil : result.first + end + + def execute(sql, name = nil, retries = 2) #:nodoc: + log(sql, name) { @connection.query(sql) } + rescue ActiveRecord::StatementInvalid => exception + if exception.message.split(":").first =~ /Packets out of order/ + raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings." + else + raise + end + end + + def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: + execute(sql, name = nil) + id_value || @connection.insert_id + end + + def update(sql, name = nil) #:nodoc: + execute(sql, name) + @connection.affected_rows + end + + alias_method :delete, :update #:nodoc: + + + def begin_db_transaction #:nodoc: + execute "BEGIN" + rescue Exception + # Transactions aren't supported + end + + def commit_db_transaction #:nodoc: + execute "COMMIT" + rescue Exception + # Transactions aren't supported + end + + def rollback_db_transaction #:nodoc: + execute "ROLLBACK" + rescue Exception + # Transactions aren't supported + end + + + def add_limit_offset!(sql, options) #:nodoc + if limit = options[:limit] + unless offset = options[:offset] + sql << " LIMIT #{limit}" + else + sql << " LIMIT #{offset}, #{limit}" + end + end + end + + + # SCHEMA STATEMENTS ======================================== + + def structure_dump #:nodoc: + if supports_views? + sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" + else + sql = "SHOW TABLES" + end + + select_all(sql).inject("") do |structure, table| + table.delete('Table_type') + structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n" + end + end + + def recreate_database(name) #:nodoc: + drop_database(name) + create_database(name) + end + + def create_database(name) #:nodoc: + execute "CREATE DATABASE `#{name}`" + end + + def drop_database(name) #:nodoc: + execute "DROP DATABASE IF EXISTS `#{name}`" + end + + def current_database + select_one("SELECT DATABASE() as db")["db"] + end + + def tables(name = nil) #:nodoc: + tables = [] + execute("SHOW TABLES", name).each { |field| tables << field[0] } + tables + end + + def indexes(table_name, name = nil)#:nodoc: + indexes = [] + current_index = nil + execute("SHOW KEYS FROM #{table_name}", name).each do |row| + if current_index != row[2] + next if row[2] == "PRIMARY" # skip the primary key + current_index = row[2] + indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", []) + end + + indexes.last.columns << row[4] + end + indexes + end + + def columns(table_name, name = nil)#:nodoc: + sql = "SHOW FIELDS FROM #{table_name}" + columns = [] + execute(sql, name).each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") } + columns + end + + def create_table(name, options = {}) #:nodoc: + super(name, {:options => "ENGINE=InnoDB"}.merge(options)) + end + + def rename_table(name, new_name) + execute "RENAME TABLE #{name} TO #{new_name}" + end + + def change_column_default(table_name, column_name, default) #:nodoc: + current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"] + + change_column(table_name, column_name, current_type, { :default => default }) + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + options[:default] ||= select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Default"] + + change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit])}" + add_column_options!(change_column_sql, options) + execute(change_column_sql) + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"] + execute "ALTER TABLE #{table_name} CHANGE #{column_name} #{new_column_name} #{current_type}" + end + + + private + def connect + encoding = @config[:encoding] + if encoding + @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil + end + @connection.real_connect(*@connection_options) + execute("SET NAMES '#{encoding}'") if encoding + end + + def select(sql, name = nil) + @connection.query_with_result = true + result = execute(sql, name) + rows = [] + if @null_values_in_each_hash + result.each_hash { |row| rows << row } + else + all_fields = result.fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields } + result.each_hash { |row| rows << all_fields.dup.update(row) } + end + result.free + rows + end + + def supports_views? + version[0] >= 5 + end + + def version + @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/openbase_adapter.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/openbase_adapter.rb new file mode 100644 index 00000000..e1372783 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/openbase_adapter.rb @@ -0,0 +1,349 @@ +require 'active_record/connection_adapters/abstract_adapter' + +module ActiveRecord + class Base + # Establishes a connection to the database that's used by all Active Record objects + def self.openbase_connection(config) # :nodoc: + require_library_or_gem 'openbase' unless self.class.const_defined?(:OpenBase) + + config = config.symbolize_keys + host = config[:host] + username = config[:username].to_s + password = config[:password].to_s + + + if config.has_key?(:database) + database = config[:database] + else + raise ArgumentError, "No database specified. Missing argument: database." + end + + oba = ConnectionAdapters::OpenBaseAdapter.new( + OpenBase.new(database, host, username, password), logger + ) + + oba + end + + end + + module ConnectionAdapters + class OpenBaseColumn < Column #:nodoc: + private + def simplified_type(field_type) + return :integer if field_type.downcase =~ /long/ + return :float if field_type.downcase == "money" + return :binary if field_type.downcase == "object" + super + end + end + # The OpenBase adapter works with the Ruby/Openbase driver by Tetsuya Suzuki. + # http://www.spice-of-life.net/ruby-openbase/ (needs version 0.7.3+) + # + # Options: + # + # * :host -- Defaults to localhost + # * :username -- Defaults to nothing + # * :password -- Defaults to nothing + # * :database -- The name of the database. No default, must be provided. + # + # The OpenBase adapter will make use of OpenBase's ability to generate unique ids + # for any column with an unique index applied. Thus, if the value of a primary + # key is not specified at the time an INSERT is performed, the adapter will prefetch + # a unique id for the primary key. This prefetching is also necessary in order + # to return the id after an insert. + # + # Caveat: Operations involving LIMIT and OFFSET do not yet work! + # + # Maintainer: derrickspell@cdmplus.com + class OpenBaseAdapter < AbstractAdapter + def adapter_name + 'OpenBase' + end + + def native_database_types + { + :primary_key => "integer UNIQUE INDEX DEFAULT _rowid", + :string => { :name => "char", :limit => 4096 }, + :text => { :name => "text" }, + :integer => { :name => "integer" }, + :float => { :name => "float" }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "timestamp" }, + :time => { :name => "time" }, + :date => { :name => "date" }, + :binary => { :name => "object" }, + :boolean => { :name => "boolean" } + } + end + + def supports_migrations? + false + end + + def prefetch_primary_key?(table_name = nil) + true + end + + def default_sequence_name(table_name, primary_key) # :nodoc: + "#{table_name} #{primary_key}" + end + + def next_sequence_value(sequence_name) + ary = sequence_name.split(' ') + if (!ary[1]) then + ary[0] =~ /(\w+)_nonstd_seq/ + ary[0] = $1 + end + @connection.unique_row_id(ary[0], ary[1]) + end + + + # QUOTING ================================================== + + def quote(value, column = nil) + if value.kind_of?(String) && column && column.type == :binary + "'#{@connection.insert_binary(value)}'" + else + super + end + end + + def quoted_true + "1" + end + + def quoted_false + "0" + end + + + + # DATABASE STATEMENTS ====================================== + + def add_limit_offset!(sql, options) #:nodoc + if limit = options[:limit] + unless offset = options[:offset] + sql << " RETURN RESULTS #{limit}" + else + limit = limit + offset + sql << " RETURN RESULTS #{offset} TO #{limit}" + end + end + end + + def select_all(sql, name = nil) #:nodoc: + select(sql, name) + end + + def select_one(sql, name = nil) #:nodoc: + add_limit_offset!(sql,{:limit => 1}) + results = select(sql, name) + results.first if results + end + + def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: + execute(sql, name) + update_nulls_after_insert(sql, name, pk, id_value, sequence_name) + id_value + end + + def execute(sql, name = nil) #:nodoc: + log(sql, name) { @connection.execute(sql) } + end + + def update(sql, name = nil) #:nodoc: + execute(sql, name).rows_affected + end + + alias_method :delete, :update #:nodoc: +#=begin + def begin_db_transaction #:nodoc: + execute "START TRANSACTION" + rescue Exception + # Transactions aren't supported + end + + def commit_db_transaction #:nodoc: + execute "COMMIT" + rescue Exception + # Transactions aren't supported + end + + def rollback_db_transaction #:nodoc: + execute "ROLLBACK" + rescue Exception + # Transactions aren't supported + end +#=end + + # SCHEMA STATEMENTS ======================================== + + # Return the list of all tables in the schema search path. + def tables(name = nil) #:nodoc: + tables = @connection.tables + tables.reject! { |t| /\A_SYS_/ === t } + tables + end + + def columns(table_name, name = nil) #:nodoc: + sql = "SELECT * FROM _sys_tables " + sql << "WHERE tablename='#{table_name}' AND INDEXOF(fieldname,'_')<>0 " + sql << "ORDER BY columnNumber" + columns = [] + select_all(sql, name).each do |row| + columns << OpenBaseColumn.new(row["fieldname"], + default_value(row["defaultvalue"]), + sql_type_name(row["typename"],row["length"]), + row["notnull"] + ) + # breakpoint() if row["fieldname"] == "content" + end + columns + end + + def indexes(table_name, name = nil)#:nodoc: + sql = "SELECT fieldname, notnull, searchindex, uniqueindex, clusteredindex FROM _sys_tables " + sql << "WHERE tablename='#{table_name}' AND INDEXOF(fieldname,'_')<>0 " + sql << "AND primarykey=0 " + sql << "AND (searchindex=1 OR uniqueindex=1 OR clusteredindex=1) " + sql << "ORDER BY columnNumber" + indexes = [] + execute(sql, name).each do |row| + indexes << IndexDefinition.new(table_name,index_name(row),row[3]==1,[row[0]]) + end + indexes + end + + + private + def select(sql, name = nil) + sql = translate_sql(sql) + results = execute(sql, name) + + date_cols = [] + col_names = [] + results.column_infos.each do |info| + col_names << info.name + date_cols << info.name if info.type == "date" + end + + rows = [] + if ( results.rows_affected ) + results.each do |row| # loop through result rows + hashed_row = {} + row.each_index do |index| + hashed_row["#{col_names[index]}"] = row[index] unless col_names[index] == "_rowid" + end + date_cols.each do |name| + unless hashed_row["#{name}"].nil? or hashed_row["#{name}"].empty? + hashed_row["#{name}"] = Date.parse(hashed_row["#{name}"],false).to_s + end + end + rows << hashed_row + end + end + rows + end + + def default_value(value) + # Boolean type values + return true if value =~ /true/ + return false if value =~ /false/ + + # Date / Time magic values + return Time.now.to_s if value =~ /^now\(\)/i + + # Empty strings should be set to null + return nil if value.empty? + + # Otherwise return what we got from OpenBase + # and hope for the best... + return value + end + + def sql_type_name(type_name, length) + return "#{type_name}(#{length})" if ( type_name =~ /char/ ) + type_name + end + + def index_name(row = []) + name = "" + name << "UNIQUE " if row[3] + name << "CLUSTERED " if row[4] + name << "INDEX" + name + end + + def translate_sql(sql) + + # Change table.* to list of columns in table + while (sql =~ /SELECT.*\s(\w+)\.\*/) + table = $1 + cols = columns(table) + if ( cols.size == 0 ) then + # Maybe this is a table alias + sql =~ /FROM(.+?)(?:LEFT|OUTER|JOIN|WHERE|GROUP|HAVING|ORDER|RETURN|$)/ + $1 =~ /[\s|,](\w+)\s+#{table}[\s|,]/ # get the tablename for this alias + cols = columns($1) + end + select_columns = [] + cols.each do |col| + select_columns << table + '.' + col.name + end + sql.gsub!(table + '.*',select_columns.join(", ")) if select_columns + end + + # Change JOIN clause to table list and WHERE condition + while (sql =~ /JOIN/) + sql =~ /((LEFT )?(OUTER )?JOIN (\w+) ON )(.+?)(?:LEFT|OUTER|JOIN|WHERE|GROUP|HAVING|ORDER|RETURN|$)/ + join_clause = $1 + $5 + is_outer_join = $3 + join_table = $4 + join_condition = $5 + join_condition.gsub!(/=/,"*") if is_outer_join + if (sql =~ /WHERE/) + sql.gsub!(/WHERE/,"WHERE (#{join_condition}) AND") + else + sql.gsub!(join_clause,"#{join_clause} WHERE #{join_condition}") + end + sql =~ /(FROM .+?)(?:LEFT|OUTER|JOIN|WHERE|$)/ + from_clause = $1 + sql.gsub!(from_clause,"#{from_clause}, #{join_table} ") + sql.gsub!(join_clause,"") + end + + # ORDER BY _rowid if no explicit ORDER BY + # This will ensure that find(:first) returns the first inserted row + if (sql !~ /(ORDER BY)|(GROUP BY)/) + if (sql =~ /RETURN RESULTS/) + sql.sub!(/RETURN RESULTS/,"ORDER BY _rowid RETURN RESULTS") + else + sql << " ORDER BY _rowid" + end + end + + sql + end + + def update_nulls_after_insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + sql =~ /INSERT INTO (\w+) \((.*)\) VALUES\s*\((.*)\)/m + table = $1 + cols = $2 + values = $3 + cols = cols.split(',') + values.gsub!(/'[^']*'/,"''") + values.gsub!(/"[^"]*"/,"\"\"") + values = values.split(',') + update_cols = [] + values.each_index { |index| update_cols << cols[index] if values[index] =~ /\s*NULL\s*/ } + update_sql = "UPDATE #{table} SET" + update_cols.each { |col| update_sql << " #{col}=NULL," unless col.empty? } + update_sql.chop!() + update_sql << " WHERE #{pk}=#{quote(id_value)}" + execute(update_sql, name + " NULL Correction") if update_cols.size > 0 + end + + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/oracle_adapter.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/oracle_adapter.rb new file mode 100644 index 00000000..f12b02fc --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/oracle_adapter.rb @@ -0,0 +1,665 @@ +# oracle_adapter.rb -- ActiveRecord adapter for Oracle 8i, 9i, 10g +# +# Original author: Graham Jenkins +# +# Current maintainer: Michael Schoen +# +######################################################################### +# +# Implementation notes: +# 1. Redefines (safely) a method in ActiveRecord to make it possible to +# implement an autonumbering solution for Oracle. +# 2. The OCI8 driver is patched to properly handle values for LONG and +# TIMESTAMP columns. The driver-author has indicated that a future +# release of the driver will obviate this patch. +# 3. LOB support is implemented through an after_save callback. +# 4. Oracle does not offer native LIMIT and OFFSET options; this +# functionality is mimiced through the use of nested selects. +# See http://asktom.oracle.com/pls/ask/f?p=4950:8:::::F4950_P8_DISPLAYID:127412348064 +# +# Do what you want with this code, at your own peril, but if any +# significant portion of my code remains then please acknowledge my +# contribution. +# portions Copyright 2005 Graham Jenkins + +require 'active_record/connection_adapters/abstract_adapter' +require 'delegate' + +begin + require_library_or_gem 'oci8' unless self.class.const_defined? :OCI8 + + module ActiveRecord + class Base + def self.oracle_connection(config) #:nodoc: + # Use OCI8AutoRecover instead of normal OCI8 driver. + ConnectionAdapters::OracleAdapter.new OCI8AutoRecover.new(config), logger + end + + # for backwards-compatibility + def self.oci_connection(config) #:nodoc: + config[:database] = config[:host] + self.oracle_connection(config) + end + + # Enable the id column to be bound into the sql later, by the adapter's insert method. + # This is preferable to inserting the hard-coded value here, because the insert method + # needs to know the id value explicitly. + alias :attributes_with_quotes_pre_oracle :attributes_with_quotes + def attributes_with_quotes(include_primary_key = true) #:nodoc: + aq = attributes_with_quotes_pre_oracle(include_primary_key) + if connection.class == ConnectionAdapters::OracleAdapter + aq[self.class.primary_key] = ":id" if include_primary_key && aq[self.class.primary_key].nil? + end + aq + end + + # After setting large objects to empty, select the OCI8::LOB + # and write back the data. + after_save :write_lobs + def write_lobs() #:nodoc: + if connection.is_a?(ConnectionAdapters::OracleAdapter) + self.class.columns.select { |c| c.type == :binary }.each { |c| + value = self[c.name] + next if value.nil? || (value == '') + lob = connection.select_one( + "SELECT #{ c.name} FROM #{ self.class.table_name } WHERE #{ self.class.primary_key} = #{quote(id)}", + 'Writable Large Object')[c.name] + lob.write value + } + end + end + + private :write_lobs + end + + + module ConnectionAdapters #:nodoc: + class OracleColumn < Column #:nodoc: + attr_reader :sql_type + + # overridden to add the concept of scale, required to differentiate + # between integer and float fields + def initialize(name, default, sql_type, limit, scale, null) + @name, @limit, @sql_type, @scale, @null = name, limit, sql_type, scale, null + + @type = simplified_type(sql_type) + @default = type_cast(default) + + @primary = nil + @text = [:string, :text].include? @type + @number = [:float, :integer].include? @type + end + + def type_cast(value) + return nil if value.nil? || value =~ /^\s*null\s*$/i + case type + when :string then value + when :integer then defined?(value.to_i) ? value.to_i : (value ? 1 : 0) + when :float then value.to_f + when :datetime then cast_to_date_or_time(value) + when :time then cast_to_time(value) + else value + end + end + + private + def simplified_type(field_type) + case field_type + when /char/i : :string + when /num|float|double|dec|real|int/i : @scale == 0 ? :integer : :float + when /date|time/i : @name =~ /_at$/ ? :time : :datetime + when /clob/i : :text + when /blob/i : :binary + end + end + + def cast_to_date_or_time(value) + return value if value.is_a? Date + return nil if value.blank? + guess_date_or_time (value.is_a? Time) ? value : cast_to_time(value) + end + + def cast_to_time(value) + return value if value.is_a? Time + time_array = ParseDate.parsedate value + time_array[0] ||= 2000; time_array[1] ||= 1; time_array[2] ||= 1; + Time.send(Base.default_timezone, *time_array) rescue nil + end + + def guess_date_or_time(value) + (value.hour == 0 and value.min == 0 and value.sec == 0) ? + Date.new(value.year, value.month, value.day) : value + end + end + + + # This is an Oracle/OCI adapter for the ActiveRecord persistence + # framework. It relies upon the OCI8 driver, which works with Oracle 8i + # and above. Most recent development has been on Debian Linux against + # a 10g database, ActiveRecord 1.12.1 and OCI8 0.1.13. + # See: http://rubyforge.org/projects/ruby-oci8/ + # + # Usage notes: + # * Key generation assumes a "${table_name}_seq" sequence is available + # for all tables; the sequence name can be changed using + # ActiveRecord::Base.set_sequence_name. When using Migrations, these + # sequences are created automatically. + # * Oracle uses DATE or TIMESTAMP datatypes for both dates and times. + # Consequently some hacks are employed to map data back to Date or Time + # in Ruby. If the column_name ends in _time it's created as a Ruby Time. + # Else if the hours/minutes/seconds are 0, I make it a Ruby Date. Else + # it's a Ruby Time. This is a bit nasty - but if you use Duck Typing + # you'll probably not care very much. In 9i and up it's tempting to + # map DATE to Date and TIMESTAMP to Time, but too many databases use + # DATE for both. Timezones and sub-second precision on timestamps are + # not supported. + # * Default values that are functions (such as "SYSDATE") are not + # supported. This is a restriction of the way ActiveRecord supports + # default values. + # * Support for Oracle8 is limited by Rails' use of ANSI join syntax, which + # is supported in Oracle9i and later. You will need to use #finder_sql for + # has_and_belongs_to_many associations to run against Oracle8. + # + # Required parameters: + # + # * :username + # * :password + # * :database + class OracleAdapter < AbstractAdapter + + def adapter_name #:nodoc: + 'Oracle' + end + + def supports_migrations? #:nodoc: + true + end + + def native_database_types #:nodoc + { + :primary_key => "NUMBER(38) NOT NULL PRIMARY KEY", + :string => { :name => "VARCHAR2", :limit => 255 }, + :text => { :name => "CLOB" }, + :integer => { :name => "NUMBER", :limit => 38 }, + :float => { :name => "NUMBER" }, + :datetime => { :name => "DATE" }, + :timestamp => { :name => "DATE" }, + :time => { :name => "DATE" }, + :date => { :name => "DATE" }, + :binary => { :name => "BLOB" }, + :boolean => { :name => "NUMBER", :limit => 1 } + } + end + + def table_alias_length + 30 + end + + # QUOTING ================================================== + # + # see: abstract/quoting.rb + + # camelCase column names need to be quoted; not that anyone using Oracle + # would really do this, but handling this case means we pass the test... + def quote_column_name(name) #:nodoc: + name =~ /[A-Z]/ ? "\"#{name}\"" : name + end + + def quote_string(string) #:nodoc: + string.gsub(/'/, "''") + end + + def quote(value, column = nil) #:nodoc: + if column && column.type == :binary + %Q{empty_#{ column.sql_type rescue 'blob' }()} + else + case value + when String : %Q{'#{quote_string(value)}'} + when NilClass : 'null' + when TrueClass : '1' + when FalseClass : '0' + when Numeric : value.to_s + when Date, Time : %Q{'#{value.strftime("%Y-%m-%d %H:%M:%S")}'} + else %Q{'#{quote_string(value.to_yaml)}'} + end + end + end + + + # CONNECTION MANAGEMENT ==================================== + # + + # Returns true if the connection is active. + def active? + # Pings the connection to check if it's still good. Note that an + # #active? method is also available, but that simply returns the + # last known state, which isn't good enough if the connection has + # gone stale since the last use. + @connection.ping + rescue OCIException + false + end + + # Reconnects to the database. + def reconnect! + @connection.reset! + rescue OCIException => e + @logger.warn "#{adapter_name} automatic reconnection failed: #{e.message}" + end + + # Disconnects from the database. + def disconnect! + @connection.logoff rescue nil + @connection.active = false + end + + + # DATABASE STATEMENTS ====================================== + # + # see: abstract/database_statements.rb + + def select_all(sql, name = nil) #:nodoc: + select(sql, name) + end + + def select_one(sql, name = nil) #:nodoc: + result = select_all(sql, name) + result.size > 0 ? result.first : nil + end + + def execute(sql, name = nil) #:nodoc: + log(sql, name) { @connection.exec sql } + end + + def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: + if pk.nil? # Who called us? What does the sql look like? No idea! + execute sql, name + elsif id_value # Pre-assigned id + log(sql, name) { @connection.exec sql } + else # Assume the sql contains a bind-variable for the id + id_value = select_one("select #{sequence_name}.nextval id from dual")['id'] + log(sql, name) { @connection.exec sql, id_value } + end + + id_value + end + + alias :update :execute #:nodoc: + alias :delete :execute #:nodoc: + + def begin_db_transaction #:nodoc: + @connection.autocommit = false + end + + def commit_db_transaction #:nodoc: + @connection.commit + ensure + @connection.autocommit = true + end + + def rollback_db_transaction #:nodoc: + @connection.rollback + ensure + @connection.autocommit = true + end + + def add_limit_offset!(sql, options) #:nodoc: + offset = options[:offset] || 0 + + if limit = options[:limit] + sql.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_ where rownum <= #{offset+limit}) where raw_rnum_ > #{offset}" + elsif offset > 0 + sql.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_) where raw_rnum_ > #{offset}" + end + end + + def default_sequence_name(table, column) #:nodoc: + "#{table}_seq" + end + + + # SCHEMA STATEMENTS ======================================== + # + # see: abstract/schema_statements.rb + + def current_database #:nodoc: + select_one("select sys_context('userenv','db_name') db from dual")["db"] + end + + def tables(name = nil) #:nodoc: + select_all("select lower(table_name) from user_tables").inject([]) do | tabs, t | + tabs << t.to_a.first.last + end + end + + def indexes(table_name, name = nil) #:nodoc: + result = select_all(<<-SQL, name) + SELECT lower(i.index_name) as index_name, i.uniqueness, lower(c.column_name) as column_name + FROM user_indexes i, user_ind_columns c + WHERE i.table_name = '#{table_name.to_s.upcase}' + AND c.index_name = i.index_name + AND i.index_name NOT IN (SELECT index_name FROM user_constraints WHERE constraint_type = 'P') + ORDER BY i.index_name, c.column_position + SQL + + current_index = nil + indexes = [] + + result.each do |row| + if current_index != row['index_name'] + indexes << IndexDefinition.new(table_name, row['index_name'], row['uniqueness'] == "UNIQUE", []) + current_index = row['index_name'] + end + + indexes.last.columns << row['column_name'] + end + + indexes + end + + def columns(table_name, name = nil) #:nodoc: + (owner, table_name) = @connection.describe(table_name) + + table_cols = %Q{ + select column_name, data_type, data_default, nullable, + decode(data_type, 'NUMBER', data_precision, + 'VARCHAR2', data_length, + null) as length, + decode(data_type, 'NUMBER', data_scale, null) as scale + from all_tab_columns + where owner = '#{owner}' + and table_name = '#{table_name}' + order by column_id + } + + select_all(table_cols, name).map do |row| + if row['data_default'] + row['data_default'].sub!(/^(.*?)\s*$/, '\1') + row['data_default'].sub!(/^'(.*)'$/, '\1') + end + OracleColumn.new( + oracle_downcase(row['column_name']), + row['data_default'], + row['data_type'], + (l = row['length']).nil? ? nil : l.to_i, + (s = row['scale']).nil? ? nil : s.to_i, + row['nullable'] == 'Y' + ) + end + end + + def create_table(name, options = {}) #:nodoc: + super(name, options) + execute "CREATE SEQUENCE #{name}_seq START WITH 10000" unless options[:id] == false + end + + def rename_table(name, new_name) #:nodoc: + execute "RENAME #{name} TO #{new_name}" + execute "RENAME #{name}_seq TO #{new_name}_seq" rescue nil + end + + def drop_table(name) #:nodoc: + super(name) + execute "DROP SEQUENCE #{name}_seq" rescue nil + end + + def remove_index(table_name, options = {}) #:nodoc: + execute "DROP INDEX #{index_name(table_name, options)}" + end + + def change_column_default(table_name, column_name, default) #:nodoc: + execute "ALTER TABLE #{table_name} MODIFY #{column_name} DEFAULT #{quote(default)}" + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + change_column_sql = "ALTER TABLE #{table_name} MODIFY #{column_name} #{type_to_sql(type, options[:limit])}" + add_column_options!(change_column_sql, options) + execute(change_column_sql) + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + execute "ALTER TABLE #{table_name} RENAME COLUMN #{column_name} to #{new_column_name}" + end + + def remove_column(table_name, column_name) #:nodoc: + execute "ALTER TABLE #{table_name} DROP COLUMN #{column_name}" + end + + def structure_dump #:nodoc: + s = select_all("select sequence_name from user_sequences").inject("") do |structure, seq| + structure << "create sequence #{seq.to_a.first.last};\n\n" + end + + select_all("select table_name from user_tables").inject(s) do |structure, table| + ddl = "create table #{table.to_a.first.last} (\n " + cols = select_all(%Q{ + select column_name, data_type, data_length, data_precision, data_scale, data_default, nullable + from user_tab_columns + where table_name = '#{table.to_a.first.last}' + order by column_id + }).map do |row| + col = "#{row['column_name'].downcase} #{row['data_type'].downcase}" + if row['data_type'] =='NUMBER' and !row['data_precision'].nil? + col << "(#{row['data_precision'].to_i}" + col << ",#{row['data_scale'].to_i}" if !row['data_scale'].nil? + col << ')' + elsif row['data_type'].include?('CHAR') + col << "(#{row['data_length'].to_i})" + end + col << " default #{row['data_default']}" if !row['data_default'].nil? + col << ' not null' if row['nullable'] == 'N' + col + end + ddl << cols.join(",\n ") + ddl << ");\n\n" + structure << ddl + end + end + + def structure_drop #:nodoc: + s = select_all("select sequence_name from user_sequences").inject("") do |drop, seq| + drop << "drop sequence #{seq.to_a.first.last};\n\n" + end + + select_all("select table_name from user_tables").inject(s) do |drop, table| + drop << "drop table #{table.to_a.first.last} cascade constraints;\n\n" + end + end + + + private + + def select(sql, name = nil) + cursor = execute(sql, name) + cols = cursor.get_col_names.map { |x| oracle_downcase(x) } + rows = [] + + while row = cursor.fetch + hash = Hash.new + + cols.each_with_index do |col, i| + hash[col] = + case row[i] + when OCI8::LOB + name == 'Writable Large Object' ? row[i]: row[i].read + when OraDate + (row[i].hour == 0 and row[i].minute == 0 and row[i].second == 0) ? + row[i].to_date : row[i].to_time + else row[i] + end unless col == 'raw_rnum_' + end + + rows << hash + end + + rows + ensure + cursor.close if cursor + end + + # Oracle column names by default are case-insensitive, but treated as upcase; + # for neatness, we'll downcase within Rails. EXCEPT that folks CAN quote + # their column names when creating Oracle tables, which makes then case-sensitive. + # I don't know anybody who does this, but we'll handle the theoretical case of a + # camelCase column name. I imagine other dbs handle this different, since there's a + # unit test that's currently failing test_oci. + def oracle_downcase(column_name) + column_name =~ /[a-z]/ ? column_name : column_name.downcase + end + + end + end + end + + + class OCI8 #:nodoc: + + # This OCI8 patch may not longer be required with the upcoming + # release of version 0.2. + class Cursor #:nodoc: + alias :define_a_column_pre_ar :define_a_column + def define_a_column(i) + case do_ocicall(@ctx) { @parms[i - 1].attrGet(OCI_ATTR_DATA_TYPE) } + when 8 : @stmt.defineByPos(i, String, 65535) # Read LONG values + when 187 : @stmt.defineByPos(i, OraDate) # Read TIMESTAMP values + when 108 + if @parms[i - 1].attrGet(OCI_ATTR_TYPE_NAME) == 'XMLTYPE' + @stmt.defineByPos(i, String, 65535) + else + raise 'unsupported datatype' + end + else define_a_column_pre_ar i + end + end + end + + # missing constant from oci8 < 0.1.14 + OCI_PTYPE_UNK = 0 unless defined?(OCI_PTYPE_UNK) + + # Uses the describeAny OCI call to find the target owner and table_name + # indicated by +name+, parsing through synonynms as necessary. Returns + # an array of [owner, table_name]. + def describe(name) + @desc ||= @@env.alloc(OCIDescribe) + @desc.attrSet(OCI_ATTR_DESC_PUBLIC, -1) if VERSION >= '0.1.14' + @desc.describeAny(@svc, name.to_s, OCI_PTYPE_UNK) + info = @desc.attrGet(OCI_ATTR_PARAM) + + case info.attrGet(OCI_ATTR_PTYPE) + when OCI_PTYPE_TABLE, OCI_PTYPE_VIEW + owner = info.attrGet(OCI_ATTR_OBJ_SCHEMA) + table_name = info.attrGet(OCI_ATTR_OBJ_NAME) + [owner, table_name] + when OCI_PTYPE_SYN + schema = info.attrGet(OCI_ATTR_SCHEMA_NAME) + name = info.attrGet(OCI_ATTR_NAME) + describe(schema + '.' + name) + end + end + + end + + + # The OracleConnectionFactory factors out the code necessary to connect and + # configure an Oracle/OCI connection. + class OracleConnectionFactory #:nodoc: + def new_connection(username, password, database) + conn = OCI8.new username, password, database + conn.exec %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'} + conn.exec %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'} rescue nil + conn.autocommit = true + conn + end + end + + + # The OCI8AutoRecover class enhances the OCI8 driver with auto-recover and + # reset functionality. If a call to #exec fails, and autocommit is turned on + # (ie., we're not in the middle of a longer transaction), it will + # automatically reconnect and try again. If autocommit is turned off, + # this would be dangerous (as the earlier part of the implied transaction + # may have failed silently if the connection died) -- so instead the + # connection is marked as dead, to be reconnected on it's next use. + class OCI8AutoRecover < DelegateClass(OCI8) #:nodoc: + attr_accessor :active + alias :active? :active + + cattr_accessor :auto_retry + class << self + alias :auto_retry? :auto_retry + end + @@auto_retry = false + + def initialize(config, factory = OracleConnectionFactory.new) + @active = true + @username, @password, @database = config[:username], config[:password], config[:database] + @factory = factory + @connection = @factory.new_connection @username, @password, @database + super @connection + end + + # Checks connection, returns true if active. Note that ping actively + # checks the connection, while #active? simply returns the last + # known state. + def ping + @connection.exec("select 1 from dual") { |r| nil } + @active = true + rescue + @active = false + raise + end + + # Resets connection, by logging off and creating a new connection. + def reset! + logoff rescue nil + begin + @connection = @factory.new_connection @username, @password, @database + __setobj__ @connection + @active = true + rescue + @active = false + raise + end + end + + # ORA-00028: your session has been killed + # ORA-01012: not logged on + # ORA-03113: end-of-file on communication channel + # ORA-03114: not connected to ORACLE + LOST_CONNECTION_ERROR_CODES = [ 28, 1012, 3113, 3114 ] + + # Adds auto-recovery functionality. + # + # See: http://www.jiubao.org/ruby-oci8/api.en.html#label-11 + def exec(sql, *bindvars) + should_retry = self.class.auto_retry? && autocommit? + + begin + @connection.exec(sql, *bindvars) + rescue OCIException => e + raise unless LOST_CONNECTION_ERROR_CODES.include?(e.code) + @active = false + raise unless should_retry + should_retry = false + reset! rescue nil + retry + end + end + + end + +rescue LoadError + # OCI8 driver is unavailable. + module ActiveRecord # :nodoc: + class Base + def self.oracle_connection(config) # :nodoc: + # Set up a reasonable error message + raise LoadError, "Oracle/OCI libraries could not be loaded." + end + def self.oci_connection(config) # :nodoc: + # Set up a reasonable error message + raise LoadError, "Oracle/OCI libraries could not be loaded." + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb new file mode 100644 index 00000000..c3b076fe --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -0,0 +1,507 @@ +require 'active_record/connection_adapters/abstract_adapter' + +module ActiveRecord + class Base + # Establishes a connection to the database that's used by all Active Record objects + def self.postgresql_connection(config) # :nodoc: + require_library_or_gem 'postgres' unless self.class.const_defined?(:PGconn) + + config = config.symbolize_keys + host = config[:host] + port = config[:port] || 5432 unless host.nil? + username = config[:username].to_s + password = config[:password].to_s + + min_messages = config[:min_messages] + + if config.has_key?(:database) + database = config[:database] + else + raise ArgumentError, "No database specified. Missing argument: database." + end + + pga = ConnectionAdapters::PostgreSQLAdapter.new( + PGconn.connect(host, port, "", "", database, username, password), logger, config + ) + + PGconn.translate_results = false if PGconn.respond_to? :translate_results= + + pga.schema_search_path = config[:schema_search_path] || config[:schema_order] + + pga + end + end + + module ConnectionAdapters + # The PostgreSQL adapter works both with the C-based (http://www.postgresql.jp/interfaces/ruby/) and the Ruby-base + # (available both as gem and from http://rubyforge.org/frs/?group_id=234&release_id=1145) drivers. + # + # Options: + # + # * :host -- Defaults to localhost + # * :port -- Defaults to 5432 + # * :username -- Defaults to nothing + # * :password -- Defaults to nothing + # * :database -- The name of the database. No default, must be provided. + # * :schema_search_path -- An optional schema search path for the connection given as a string of comma-separated schema names. This is backward-compatible with the :schema_order option. + # * :encoding -- An optional client encoding that is using in a SET client_encoding TO call on connection. + # * :min_messages -- An optional client min messages that is using in a SET client_min_messages TO call on connection. + class PostgreSQLAdapter < AbstractAdapter + def adapter_name + 'PostgreSQL' + end + + def initialize(connection, logger, config = {}) + super(connection, logger) + @config = config + configure_connection + end + + # Is this connection alive and ready for queries? + def active? + if @connection.respond_to?(:status) + @connection.status == PGconn::CONNECTION_OK + else + @connection.query 'SELECT 1' + true + end + # postgres-pr raises a NoMethodError when querying if no conn is available + rescue PGError, NoMethodError + false + end + + # Close then reopen the connection. + def reconnect! + # TODO: postgres-pr doesn't have PGconn#reset. + if @connection.respond_to?(:reset) + @connection.reset + configure_connection + end + end + + def disconnect! + # Both postgres and postgres-pr respond to :close + @connection.close rescue nil + end + + def native_database_types + { + :primary_key => "serial primary key", + :string => { :name => "character varying", :limit => 255 }, + :text => { :name => "text" }, + :integer => { :name => "integer" }, + :float => { :name => "float" }, + :datetime => { :name => "timestamp" }, + :timestamp => { :name => "timestamp" }, + :time => { :name => "time" }, + :date => { :name => "date" }, + :binary => { :name => "bytea" }, + :boolean => { :name => "boolean" } + } + end + + def supports_migrations? + true + end + + def table_alias_length + 63 + end + + # QUOTING ================================================== + + def quote(value, column = nil) + if value.kind_of?(String) && column && column.type == :binary + "'#{escape_bytea(value)}'" + else + super + end + end + + def quote_column_name(name) + %("#{name}") + end + + + # DATABASE STATEMENTS ====================================== + + def select_all(sql, name = nil) #:nodoc: + select(sql, name) + end + + def select_one(sql, name = nil) #:nodoc: + result = select(sql, name) + result.first if result + end + + def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: + execute(sql, name) + table = sql.split(" ", 4)[2] + id_value || last_insert_id(table, sequence_name || default_sequence_name(table, pk)) + end + + def query(sql, name = nil) #:nodoc: + log(sql, name) { @connection.query(sql) } + end + + def execute(sql, name = nil) #:nodoc: + log(sql, name) { @connection.exec(sql) } + end + + def update(sql, name = nil) #:nodoc: + execute(sql, name).cmdtuples + end + + alias_method :delete, :update #:nodoc: + + + def begin_db_transaction #:nodoc: + execute "BEGIN" + end + + def commit_db_transaction #:nodoc: + execute "COMMIT" + end + + def rollback_db_transaction #:nodoc: + execute "ROLLBACK" + end + + + # SCHEMA STATEMENTS ======================================== + + # Return the list of all tables in the schema search path. + def tables(name = nil) #:nodoc: + schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',') + query(<<-SQL, name).map { |row| row[0] } + SELECT tablename + FROM pg_tables + WHERE schemaname IN (#{schemas}) + SQL + end + + def indexes(table_name, name = nil) #:nodoc: + result = query(<<-SQL, name) + SELECT i.relname, d.indisunique, a.attname + FROM pg_class t, pg_class i, pg_index d, pg_attribute a + WHERE i.relkind = 'i' + AND d.indexrelid = i.oid + AND d.indisprimary = 'f' + AND t.oid = d.indrelid + AND t.relname = '#{table_name}' + AND a.attrelid = t.oid + AND ( d.indkey[0]=a.attnum OR d.indkey[1]=a.attnum + OR d.indkey[2]=a.attnum OR d.indkey[3]=a.attnum + OR d.indkey[4]=a.attnum OR d.indkey[5]=a.attnum + OR d.indkey[6]=a.attnum OR d.indkey[7]=a.attnum + OR d.indkey[8]=a.attnum OR d.indkey[9]=a.attnum ) + ORDER BY i.relname + SQL + + current_index = nil + indexes = [] + + result.each do |row| + if current_index != row[0] + indexes << IndexDefinition.new(table_name, row[0], row[1] == "t", []) + current_index = row[0] + end + + indexes.last.columns << row[2] + end + + indexes + end + + def columns(table_name, name = nil) #:nodoc: + column_definitions(table_name).collect do |name, type, default, notnull| + Column.new(name, default_value(default), translate_field_type(type), + notnull == "f") + end + end + + # Set the schema search path to a string of comma-separated schema names. + # Names beginning with $ are quoted (e.g. $user => '$user') + # See http://www.postgresql.org/docs/8.0/interactive/ddl-schemas.html + def schema_search_path=(schema_csv) #:nodoc: + if schema_csv + execute "SET search_path TO #{schema_csv}" + @schema_search_path = nil + end + end + + def schema_search_path #:nodoc: + @schema_search_path ||= query('SHOW search_path')[0][0] + end + + def default_sequence_name(table_name, pk = nil) + default_pk, default_seq = pk_and_sequence_for(table_name) + default_seq || "#{table_name}_#{pk || default_pk || 'id'}_seq" + end + + # Resets sequence to the max value of the table's pk if present. + def reset_pk_sequence!(table, pk = nil, sequence = nil) + unless pk and sequence + default_pk, default_sequence = pk_and_sequence_for(table) + pk ||= default_pk + sequence ||= default_sequence + end + if pk + if sequence + select_value <<-end_sql, 'Reset sequence' + SELECT setval('#{sequence}', (SELECT COALESCE(MAX(#{pk})+(SELECT increment_by FROM #{sequence}), (SELECT min_value FROM #{sequence})) FROM #{table}), false) + end_sql + else + @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger + end + end + end + + # Find a table's primary key and sequence. + def pk_and_sequence_for(table) + # First try looking for a sequence with a dependency on the + # given table's primary key. + result = execute(<<-end_sql, 'PK and serial sequence')[0] + SELECT attr.attname, name.nspname, seq.relname + FROM pg_class seq, + pg_attribute attr, + pg_depend dep, + pg_namespace name, + pg_constraint cons + WHERE seq.oid = dep.objid + AND seq.relnamespace = name.oid + AND seq.relkind = 'S' + AND attr.attrelid = dep.refobjid + AND attr.attnum = dep.refobjsubid + AND attr.attrelid = cons.conrelid + AND attr.attnum = cons.conkey[1] + AND cons.contype = 'p' + AND dep.refobjid = '#{table}'::regclass + end_sql + + if result.nil? or result.empty? + # If that fails, try parsing the primary key's default value. + # Support the 7.x and 8.0 nextval('foo'::text) as well as + # the 8.1+ nextval('foo'::regclass). + # TODO: assumes sequence is in same schema as table. + result = execute(<<-end_sql, 'PK and custom sequence')[0] + SELECT attr.attname, name.nspname, split_part(def.adsrc, '\\\'', 2) + FROM pg_class t + JOIN pg_namespace name ON (t.relnamespace = name.oid) + JOIN pg_attribute attr ON (t.oid = attrelid) + JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) + JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) + WHERE t.oid = '#{table}'::regclass + AND cons.contype = 'p' + AND def.adsrc ~* 'nextval' + end_sql + end + # check for existence of . in sequence name as in public.foo_sequence. if it does not exist, join the current namespace + result.last['.'] ? [result.first, result.last] : [result.first, "#{result[1]}.#{result[2]}"] + rescue + nil + end + + def rename_table(name, new_name) + execute "ALTER TABLE #{name} RENAME TO #{new_name}" + end + + def add_column(table_name, column_name, type, options = {}) + execute("ALTER TABLE #{table_name} ADD #{column_name} #{type_to_sql(type, options[:limit])}") + execute("ALTER TABLE #{table_name} ALTER #{column_name} SET NOT NULL") if options[:null] == false + change_column_default(table_name, column_name, options[:default]) unless options[:default].nil? + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + begin + execute "ALTER TABLE #{table_name} ALTER #{column_name} TYPE #{type_to_sql(type, options[:limit])}" + rescue ActiveRecord::StatementInvalid + # This is PG7, so we use a more arcane way of doing it. + begin_db_transaction + add_column(table_name, "#{column_name}_ar_tmp", type, options) + execute "UPDATE #{table_name} SET #{column_name}_ar_tmp = CAST(#{column_name} AS #{type_to_sql(type, options[:limit])})" + remove_column(table_name, column_name) + rename_column(table_name, "#{column_name}_ar_tmp", column_name) + commit_db_transaction + end + change_column_default(table_name, column_name, options[:default]) unless options[:default].nil? + end + + def change_column_default(table_name, column_name, default) #:nodoc: + execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DEFAULT '#{default}'" + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + execute "ALTER TABLE #{table_name} RENAME COLUMN #{column_name} TO #{new_column_name}" + end + + def remove_index(table_name, options) #:nodoc: + execute "DROP INDEX #{index_name(table_name, options)}" + end + + private + BYTEA_COLUMN_TYPE_OID = 17 + TIMESTAMPOID = 1114 + TIMESTAMPTZOID = 1184 + + def configure_connection + if @config[:encoding] + execute("SET client_encoding TO '#{@config[:encoding]}'") + end + if @config[:min_messages] + execute("SET client_min_messages TO '#{@config[:min_messages]}'") + end + end + + def last_insert_id(table, sequence_name) + Integer(select_value("SELECT currval('#{sequence_name}')")) + end + + def select(sql, name = nil) + res = execute(sql, name) + results = res.result + rows = [] + if results.length > 0 + fields = res.fields + results.each do |row| + hashed_row = {} + row.each_index do |cel_index| + column = row[cel_index] + + case res.type(cel_index) + when BYTEA_COLUMN_TYPE_OID + column = unescape_bytea(column) + when TIMESTAMPTZOID, TIMESTAMPOID + column = cast_to_time(column) + end + + hashed_row[fields[cel_index]] = column + end + rows << hashed_row + end + end + return rows + end + + def escape_bytea(s) + if PGconn.respond_to? :escape_bytea + self.class.send(:define_method, :escape_bytea) do |s| + PGconn.escape_bytea(s) if s + end + else + self.class.send(:define_method, :escape_bytea) do |s| + if s + result = '' + s.each_byte { |c| result << sprintf('\\\\%03o', c) } + result + end + end + end + escape_bytea(s) + end + + def unescape_bytea(s) + if PGconn.respond_to? :unescape_bytea + self.class.send(:define_method, :unescape_bytea) do |s| + PGconn.unescape_bytea(s) if s + end + else + self.class.send(:define_method, :unescape_bytea) do |s| + if s + result = '' + i, max = 0, s.size + while i < max + char = s[i] + if char == ?\\ + if s[i+1] == ?\\ + char = ?\\ + i += 1 + else + char = s[i+1..i+3].oct + i += 3 + end + end + result << char + i += 1 + end + result + end + end + end + unescape_bytea(s) + end + + # Query a table's column names, default values, and types. + # + # The underlying query is roughly: + # SELECT column.name, column.type, default.value + # FROM column LEFT JOIN default + # ON column.table_id = default.table_id + # AND column.num = default.column_num + # WHERE column.table_id = get_table_id('table_name') + # AND column.num > 0 + # AND NOT column.is_dropped + # ORDER BY column.num + # + # If the table name is not prefixed with a schema, the database will + # take the first match from the schema search path. + # + # Query implementation notes: + # - format_type includes the column size constraint, e.g. varchar(50) + # - ::regclass is a function that gives the id for a table name + def column_definitions(table_name) + query <<-end_sql + SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull + FROM pg_attribute a LEFT JOIN pg_attrdef d + ON a.attrelid = d.adrelid AND a.attnum = d.adnum + WHERE a.attrelid = '#{table_name}'::regclass + AND a.attnum > 0 AND NOT a.attisdropped + ORDER BY a.attnum + end_sql + end + + # Translate PostgreSQL-specific types into simplified SQL types. + # These are special cases; standard types are handled by + # ConnectionAdapters::Column#simplified_type. + def translate_field_type(field_type) + # Match the beginning of field_type since it may have a size constraint on the end. + case field_type + when /^timestamp/i then 'datetime' + when /^real|^money/i then 'float' + when /^interval/i then 'string' + # geometric types (the line type is currently not implemented in postgresql) + when /^(?:point|lseg|box|"?path"?|polygon|circle)/i then 'string' + when /^bytea/i then 'binary' + else field_type # Pass through standard types. + end + end + + def default_value(value) + # Boolean types + return "t" if value =~ /true/i + return "f" if value =~ /false/i + + # Char/String/Bytea type values + return $1 if value =~ /^'(.*)'::(bpchar|text|character varying|bytea)$/ + + # Numeric values + return value if value =~ /^-?[0-9]+(\.[0-9]*)?/ + + # Fixed dates / times + return $1 if value =~ /^'(.+)'::(date|timestamp)/ + + # Anything else is blank, some user type, or some function + # and we can't know the value of that, so return nil. + return nil + end + + # Only needed for DateTime instances + def cast_to_time(value) + return value unless value.class == DateTime + v = value + time_array = [v.year, v.month, v.day, v.hour, v.min, v.sec] + Time.send(Base.default_timezone, *time_array) rescue nil + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb new file mode 100644 index 00000000..ba219b97 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -0,0 +1,371 @@ +# Author: Luke Holden +# Updated for SQLite3: Jamis Buck + +require 'active_record/connection_adapters/abstract_adapter' + +module ActiveRecord + class Base + class << self + # sqlite3 adapter reuses sqlite_connection. + def sqlite3_connection(config) # :nodoc: + parse_config!(config) + + unless self.class.const_defined?(:SQLite3) + require_library_or_gem(config[:adapter]) + end + + db = SQLite3::Database.new( + config[:database], + :results_as_hash => true, + :type_translation => false + ) + ConnectionAdapters::SQLiteAdapter.new(db, logger) + end + + # Establishes a connection to the database that's used by all Active Record objects + def sqlite_connection(config) # :nodoc: + parse_config!(config) + + unless self.class.const_defined?(:SQLite) + require_library_or_gem(config[:adapter]) + + db = SQLite::Database.new(config[:database], 0) + db.show_datatypes = "ON" if !defined? SQLite::Version + db.results_as_hash = true if defined? SQLite::Version + db.type_translation = false + + # "Downgrade" deprecated sqlite API + if SQLite.const_defined?(:Version) + ConnectionAdapters::SQLite2Adapter.new(db, logger) + else + ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger) + end + end + end + + private + def parse_config!(config) + config[:database] ||= config[:dbfile] + # Require database. + unless config[:database] + raise ArgumentError, "No database file specified. Missing argument: database" + end + + # Allow database path relative to RAILS_ROOT, but only if + # the database path is not the special path that tells + # Sqlite build a database only in memory. + if Object.const_defined?(:RAILS_ROOT) && ':memory:' != config[:database] + config[:database] = File.expand_path(config[:database], RAILS_ROOT) + end + end + end + end + + module ConnectionAdapters #:nodoc: + class SQLiteColumn < Column #:nodoc: + class << self + def string_to_binary(value) + value.gsub(/\0|\%/) do |b| + case b + when "\0" then "%00" + when "%" then "%25" + end + end + end + + def binary_to_string(value) + value.gsub(/%00|%25/) do |b| + case b + when "%00" then "\0" + when "%25" then "%" + end + end + end + end + end + + # The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby drivers (available both as gems and + # from http://rubyforge.org/projects/sqlite-ruby/). + # + # Options: + # + # * :database -- Path to the database file. + class SQLiteAdapter < AbstractAdapter + def adapter_name #:nodoc: + 'SQLite' + end + + def supports_migrations? #:nodoc: + true + end + + def supports_count_distinct? #:nodoc: + false + end + + def native_database_types #:nodoc: + { + :primary_key => "INTEGER PRIMARY KEY NOT NULL", + :string => { :name => "varchar", :limit => 255 }, + :text => { :name => "text" }, + :integer => { :name => "integer" }, + :float => { :name => "float" }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "datetime" }, + :time => { :name => "datetime" }, + :date => { :name => "date" }, + :binary => { :name => "blob" }, + :boolean => { :name => "boolean" } + } + end + + + # QUOTING ================================================== + + def quote_string(s) #:nodoc: + @connection.class.quote(s) + end + + def quote_column_name(name) #:nodoc: + %Q("#{name}") + end + + + # DATABASE STATEMENTS ====================================== + + def execute(sql, name = nil) #:nodoc: + catch_schema_changes { log(sql, name) { @connection.execute(sql) } } + end + + def update(sql, name = nil) #:nodoc: + execute(sql, name) + @connection.changes + end + + def delete(sql, name = nil) #:nodoc: + sql += " WHERE 1=1" unless sql =~ /WHERE/i + execute(sql, name) + @connection.changes + end + + def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: + execute(sql, name = nil) + id_value || @connection.last_insert_row_id + end + + def select_all(sql, name = nil) #:nodoc: + execute(sql, name).map do |row| + record = {} + row.each_key do |key| + if key.is_a?(String) + record[key.sub(/^\w+\./, '')] = row[key] + end + end + record + end + end + + def select_one(sql, name = nil) #:nodoc: + result = select_all(sql, name) + result.nil? ? nil : result.first + end + + + def begin_db_transaction #:nodoc: + catch_schema_changes { @connection.transaction } + end + + def commit_db_transaction #:nodoc: + catch_schema_changes { @connection.commit } + end + + def rollback_db_transaction #:nodoc: + catch_schema_changes { @connection.rollback } + end + + + # SCHEMA STATEMENTS ======================================== + + def tables(name = nil) #:nodoc: + execute("SELECT name FROM sqlite_master WHERE type = 'table'", name).map do |row| + row[0] + end + end + + def columns(table_name, name = nil) #:nodoc: + table_structure(table_name).map do |field| + SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'] == "0") + end + end + + def indexes(table_name, name = nil) #:nodoc: + execute("PRAGMA index_list(#{table_name})", name).map do |row| + index = IndexDefinition.new(table_name, row['name']) + index.unique = row['unique'] != '0' + index.columns = execute("PRAGMA index_info('#{index.name}')").map { |col| col['name'] } + index + end + end + + def primary_key(table_name) #:nodoc: + column = table_structure(table_name).find {|field| field['pk'].to_i == 1} + column ? column['name'] : nil + end + + def remove_index(table_name, options={}) #:nodoc: + execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}" + end + + def rename_table(name, new_name) + move_table(name, new_name) + end + + def add_column(table_name, column_name, type, options = {}) #:nodoc: + alter_table(table_name) do |definition| + definition.column(column_name, type, options) + end + end + + def remove_column(table_name, column_name) #:nodoc: + alter_table(table_name) do |definition| + definition.columns.delete(definition[column_name]) + end + end + + def change_column_default(table_name, column_name, default) #:nodoc: + alter_table(table_name) do |definition| + definition[column_name].default = default + end + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + alter_table(table_name) do |definition| + definition[column_name].instance_eval do + self.type = type + self.limit = options[:limit] if options[:limit] + self.default = options[:default] if options[:default] + end + end + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + alter_table(table_name, :rename => {column_name => new_column_name}) + end + + + protected + def table_structure(table_name) + returning structure = execute("PRAGMA table_info(#{table_name})") do + raise ActiveRecord::StatementInvalid if structure.empty? + end + end + + def alter_table(table_name, options = {}) #:nodoc: + altered_table_name = "altered_#{table_name}" + caller = lambda {|definition| yield definition if block_given?} + + transaction do + move_table(table_name, altered_table_name, + options.merge(:temporary => true)) + move_table(altered_table_name, table_name, &caller) + end + end + + def move_table(from, to, options = {}, &block) #:nodoc: + copy_table(from, to, options, &block) + drop_table(from) + end + + def copy_table(from, to, options = {}) #:nodoc: + create_table(to, options) do |@definition| + columns(from).each do |column| + column_name = options[:rename] ? + (options[:rename][column.name] || + options[:rename][column.name.to_sym] || + column.name) : column.name + + @definition.column(column_name, column.type, + :limit => column.limit, :default => column.default, + :null => column.null) + end + @definition.primary_key(primary_key(from)) + yield @definition if block_given? + end + + copy_table_indexes(from, to) + copy_table_contents(from, to, + @definition.columns.map {|column| column.name}, + options[:rename] || {}) + end + + def copy_table_indexes(from, to) #:nodoc: + indexes(from).each do |index| + name = index.name + if to == "altered_#{from}" + name = "temp_#{name}" + elsif from == "altered_#{to}" + name = name[5..-1] + end + + opts = { :name => name } + opts[:unique] = true if index.unique + add_index(to, index.columns, opts) + end + end + + def copy_table_contents(from, to, columns, rename = {}) #:nodoc: + column_mappings = Hash[*columns.map {|name| [name, name]}.flatten] + rename.inject(column_mappings) {|map, a| map[a.last] = a.first; map} + + @connection.execute "SELECT * FROM #{from}" do |row| + sql = "INSERT INTO #{to} VALUES (" + sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' + sql << ')' + @connection.execute sql + end + end + + def catch_schema_changes + return yield + rescue ActiveRecord::StatementInvalid => exception + if exception.message =~ /database schema has changed/ + reconnect! + retry + else + raise + end + end + end + + class SQLite2Adapter < SQLiteAdapter # :nodoc: + # SQLite 2 does not support COUNT(DISTINCT) queries: + # + # select COUNT(DISTINCT ArtistID) from CDs; + # + # In order to get the number of artists we execute the following statement + # + # SELECT COUNT(ArtistID) FROM (SELECT DISTINCT ArtistID FROM CDs); + def execute(sql, name = nil) #:nodoc: + super(rewrite_count_distinct_queries(sql), name) + end + + def rewrite_count_distinct_queries(sql) + if sql =~ /count\(distinct ([^\)]+)\)( AS \w+)? (.*)/i + distinct_column = $1 + distinct_query = $3 + column_name = distinct_column.split('.').last + "SELECT COUNT(#{column_name}) FROM (SELECT DISTINCT #{distinct_column} #{distinct_query})" + else + sql + end + end + end + + class DeprecatedSQLiteAdapter < SQLite2Adapter # :nodoc: + def insert(sql, name = nil, pk = nil, id_value = nil) + execute(sql, name = nil) + id_value || @connection.last_insert_rowid + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb new file mode 100644 index 00000000..0790dc18 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -0,0 +1,563 @@ +require 'active_record/connection_adapters/abstract_adapter' + +# sqlserver_adapter.rb -- ActiveRecord adapter for Microsoft SQL Server +# +# Author: Joey Gibson +# Date: 10/14/2004 +# +# Modifications: DeLynn Berry +# Date: 3/22/2005 +# +# Modifications (ODBC): Mark Imbriaco +# Date: 6/26/2005 +# +# Current maintainer: Ryan Tomayko +# +# Modifications (Migrations): Tom Ward +# Date: 27/10/2005 +# + +module ActiveRecord + class Base + def self.sqlserver_connection(config) #:nodoc: + require_library_or_gem 'dbi' unless self.class.const_defined?(:DBI) + + config = config.symbolize_keys + + mode = config[:mode] ? config[:mode].to_s.upcase : 'ADO' + username = config[:username] ? config[:username].to_s : 'sa' + password = config[:password] ? config[:password].to_s : '' + autocommit = config.key?(:autocommit) ? config[:autocommit] : true + if mode == "ODBC" + raise ArgumentError, "Missing DSN. Argument ':dsn' must be set in order for this adapter to work." unless config.has_key?(:dsn) + dsn = config[:dsn] + driver_url = "DBI:ODBC:#{dsn}" + else + raise ArgumentError, "Missing Database. Argument ':database' must be set in order for this adapter to work." unless config.has_key?(:database) + database = config[:database] + host = config[:host] ? config[:host].to_s : 'localhost' + driver_url = "DBI:ADO:Provider=SQLOLEDB;Data Source=#{host};Initial Catalog=#{database};User Id=#{username};Password=#{password};" + end + conn = DBI.connect(driver_url, username, password) + conn["AutoCommit"] = autocommit + ConnectionAdapters::SQLServerAdapter.new(conn, logger, [driver_url, username, password]) + end + end # class Base + + module ConnectionAdapters + class ColumnWithIdentity < Column# :nodoc: + attr_reader :identity, :is_special, :scale + + def initialize(name, default, sql_type = nil, is_identity = false, null = true, scale_value = 0) + super(name, default, sql_type, null) + @identity = is_identity + @is_special = sql_type =~ /text|ntext|image/i ? true : false + @scale = scale_value + # SQL Server only supports limits on *char and float types + @limit = nil unless @type == :float or @type == :string + end + + def simplified_type(field_type) + case field_type + when /int|bigint|smallint|tinyint/i then :integer + when /float|double|decimal|money|numeric|real|smallmoney/i then @scale == 0 ? :integer : :float + when /datetime|smalldatetime/i then :datetime + when /timestamp/i then :timestamp + when /time/i then :time + when /text|ntext/i then :text + when /binary|image|varbinary/i then :binary + when /char|nchar|nvarchar|string|varchar/i then :string + when /bit/i then :boolean + when /uniqueidentifier/i then :string + end + end + + def type_cast(value) + return nil if value.nil? || value =~ /^\s*null\s*$/i + case type + when :string then value + when :integer then value == true || value == false ? value == true ? 1 : 0 : value.to_i + when :float then value.to_f + when :datetime then cast_to_datetime(value) + when :timestamp then cast_to_time(value) + when :time then cast_to_time(value) + when :date then cast_to_datetime(value) + when :boolean then value == true or (value =~ /^t(rue)?$/i) == 0 or value.to_s == '1' + else value + end + end + + def cast_to_time(value) + return value if value.is_a?(Time) + time_array = ParseDate.parsedate(value) + time_array[0] ||= 2000 + time_array[1] ||= 1 + time_array[2] ||= 1 + Time.send(Base.default_timezone, *time_array) rescue nil + end + + def cast_to_datetime(value) + if value.is_a?(Time) + if value.year != 0 and value.month != 0 and value.day != 0 + return value + else + return Time.mktime(2000, 1, 1, value.hour, value.min, value.sec) rescue nil + end + end + return cast_to_time(value) if value.is_a?(Date) or value.is_a?(String) rescue nil + value + end + + # These methods will only allow the adapter to insert binary data with a length of 7K or less + # because of a SQL Server statement length policy. + def self.string_to_binary(value) + value.gsub(/(\r|\n|\0|\x1a)/) do + case $1 + when "\r" then "%00" + when "\n" then "%01" + when "\0" then "%02" + when "\x1a" then "%03" + end + end + end + + def self.binary_to_string(value) + value.gsub(/(%00|%01|%02|%03)/) do + case $1 + when "%00" then "\r" + when "%01" then "\n" + when "%02\0" then "\0" + when "%03" then "\x1a" + end + end + end + end + + # In ADO mode, this adapter will ONLY work on Windows systems, + # since it relies on Win32OLE, which, to my knowledge, is only + # available on Windows. + # + # This mode also relies on the ADO support in the DBI module. If you are using the + # one-click installer of Ruby, then you already have DBI installed, but + # the ADO module is *NOT* installed. You will need to get the latest + # source distribution of Ruby-DBI from http://ruby-dbi.sourceforge.net/ + # unzip it, and copy the file + # src/lib/dbd_ado/ADO.rb + # to + # X:/Ruby/lib/ruby/site_ruby/1.8/DBD/ADO/ADO.rb + # (you will more than likely need to create the ADO directory). + # Once you've installed that file, you are ready to go. + # + # In ODBC mode, the adapter requires the ODBC support in the DBI module which requires + # the Ruby ODBC module. Ruby ODBC 0.996 was used in development and testing, + # and it is available at http://www.ch-werner.de/rubyodbc/ + # + # Options: + # + # * :mode -- ADO or ODBC. Defaults to ADO. + # * :username -- Defaults to sa. + # * :password -- Defaults to empty string. + # + # ADO specific options: + # + # * :host -- Defaults to localhost. + # * :database -- The name of the database. No default, must be provided. + # + # ODBC specific options: + # + # * :dsn -- Defaults to nothing. + # + # ADO code tested on Windows 2000 and higher systems, + # running ruby 1.8.2 (2004-07-29) [i386-mswin32], and SQL Server 2000 SP3. + # + # ODBC code tested on a Fedora Core 4 system, running FreeTDS 0.63, + # unixODBC 2.2.11, Ruby ODBC 0.996, Ruby DBI 0.0.23 and Ruby 1.8.2. + # [Linux strongmad 2.6.11-1.1369_FC4 #1 Thu Jun 2 22:55:56 EDT 2005 i686 i686 i386 GNU/Linux] + class SQLServerAdapter < AbstractAdapter + + def initialize(connection, logger, connection_options=nil) + super(connection, logger) + @connection_options = connection_options + end + + def native_database_types + { + :primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY", + :string => { :name => "varchar", :limit => 255 }, + :text => { :name => "text" }, + :integer => { :name => "int" }, + :float => { :name => "float", :limit => 8 }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "datetime" }, + :time => { :name => "datetime" }, + :date => { :name => "datetime" }, + :binary => { :name => "image"}, + :boolean => { :name => "bit"} + } + end + + def adapter_name + 'SQLServer' + end + + def supports_migrations? #:nodoc: + true + end + + # CONNECTION MANAGEMENT ====================================# + + # Returns true if the connection is active. + def active? + @connection.execute("SELECT 1") { } + true + rescue DBI::DatabaseError, DBI::InterfaceError + false + end + + # Reconnects to the database, returns false if no connection could be made. + def reconnect! + disconnect! + @connection = DBI.connect(*@connection_options) + rescue DBI::DatabaseError => e + @logger.warn "#{adapter_name} reconnection failed: #{e.message}" if @logger + false + end + + # Disconnects from the database + + def disconnect! + @connection.disconnect rescue nil + end + + def select_all(sql, name = nil) + select(sql, name) + end + + def select_one(sql, name = nil) + add_limit!(sql, :limit => 1) + result = select(sql, name) + result.nil? ? nil : result.first + end + + def columns(table_name, name = nil) + return [] if table_name.blank? + table_name = table_name.to_s if table_name.is_a?(Symbol) + table_name = table_name.split('.')[-1] unless table_name.nil? + sql = "SELECT COLUMN_NAME as ColName, COLUMN_DEFAULT as DefaultValue, DATA_TYPE as ColType, IS_NULLABLE As IsNullable, COL_LENGTH('#{table_name}', COLUMN_NAME) as Length, COLUMNPROPERTY(OBJECT_ID('#{table_name}'), COLUMN_NAME, 'IsIdentity') as IsIdentity, NUMERIC_SCALE as Scale FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '#{table_name}'" + # Comment out if you want to have the Columns select statment logged. + # Personally, I think it adds unnecessary bloat to the log. + # If you do comment it out, make sure to un-comment the "result" line that follows + result = log(sql, name) { @connection.select_all(sql) } + #result = @connection.select_all(sql) + columns = [] + result.each do |field| + default = field[:DefaultValue].to_s.gsub!(/[()\']/,"") =~ /null/ ? nil : field[:DefaultValue] + type = "#{field[:ColType]}(#{field[:Length]})" + is_identity = field[:IsIdentity] == 1 + is_nullable = field[:IsNullable] == 'YES' + columns << ColumnWithIdentity.new(field[:ColName], default, type, is_identity, is_nullable, field[:Scale]) + end + columns + end + + def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + begin + table_name = get_table_name(sql) + col = get_identity_column(table_name) + ii_enabled = false + + if col != nil + if query_contains_identity_column(sql, col) + begin + execute enable_identity_insert(table_name, true) + ii_enabled = true + rescue Exception => e + raise ActiveRecordError, "IDENTITY_INSERT could not be turned ON" + end + end + end + log(sql, name) do + @connection.execute(sql) + id_value || select_one("SELECT @@IDENTITY AS Ident")["Ident"] + end + ensure + if ii_enabled + begin + execute enable_identity_insert(table_name, false) + rescue Exception => e + raise ActiveRecordError, "IDENTITY_INSERT could not be turned OFF" + end + end + end + end + + def execute(sql, name = nil) + if sql =~ /^\s*INSERT/i + insert(sql, name) + elsif sql =~ /^\s*UPDATE|^\s*DELETE/i + log(sql, name) do + @connection.execute(sql) + retVal = select_one("SELECT @@ROWCOUNT AS AffectedRows")["AffectedRows"] + end + else + log(sql, name) { @connection.execute(sql) } + end + end + + def update(sql, name = nil) + execute(sql, name) + end + alias_method :delete, :update + + def begin_db_transaction + @connection["AutoCommit"] = false + rescue Exception => e + @connection["AutoCommit"] = true + end + + def commit_db_transaction + @connection.commit + ensure + @connection["AutoCommit"] = true + end + + def rollback_db_transaction + @connection.rollback + ensure + @connection["AutoCommit"] = true + end + + def quote(value, column = nil) + case value + when String + if column && column.type == :binary && column.class.respond_to?(:string_to_binary) + "'#{quote_string(column.class.string_to_binary(value))}'" + else + "'#{quote_string(value)}'" + end + when NilClass then "NULL" + when TrueClass then '1' + when FalseClass then '0' + when Float, Fixnum, Bignum then value.to_s + when Date then "'#{value.to_s}'" + when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'" + else "'#{quote_string(value.to_yaml)}'" + end + end + + def quote_string(string) + string.gsub(/\'/, "''") + end + + def quoted_true + "1" + end + + def quoted_false + "0" + end + + def quote_column_name(name) + "[#{name}]" + end + + def add_limit_offset!(sql, options) + if options[:limit] and options[:offset] + total_rows = @connection.select_all("SELECT count(*) as TotalRows from (#{sql.gsub(/\bSELECT\b/i, "SELECT TOP 1000000000")}) tally")[0][:TotalRows].to_i + if (options[:limit] + options[:offset]) >= total_rows + options[:limit] = (total_rows - options[:offset] >= 0) ? (total_rows - options[:offset]) : 0 + end + sql.sub!(/^\s*SELECT/i, "SELECT * FROM (SELECT TOP #{options[:limit]} * FROM (SELECT TOP #{options[:limit] + options[:offset]} ") + sql << ") AS tmp1" + if options[:order] + options[:order] = options[:order].split(',').map do |field| + parts = field.split(" ") + tc = parts[0] + if sql =~ /\.\[/ and tc =~ /\./ # if column quoting used in query + tc.gsub!(/\./, '\\.\\[') + tc << '\\]' + end + if sql =~ /#{tc} AS (t\d_r\d\d?)/ + parts[0] = $1 + end + parts.join(' ') + end.join(', ') + sql << " ORDER BY #{change_order_direction(options[:order])}) AS tmp2 ORDER BY #{options[:order]}" + else + sql << " ) AS tmp2" + end + elsif sql !~ /^\s*SELECT (@@|COUNT\()/i + sql.sub!(/^\s*SELECT([\s]*distinct)?/i) do + "SELECT#{$1} TOP #{options[:limit]}" + end unless options[:limit].nil? + end + end + + def recreate_database(name) + drop_database(name) + create_database(name) + end + + def drop_database(name) + execute "DROP DATABASE #{name}" + end + + def create_database(name) + execute "CREATE DATABASE #{name}" + end + + def current_database + @connection.select_one("select DB_NAME()")[0] + end + + def tables(name = nil) + execute("SELECT table_name from information_schema.tables WHERE table_type = 'BASE TABLE'", name).inject([]) do |tables, field| + table_name = field[0] + tables << table_name unless table_name == 'dtproperties' + tables + end + end + + def indexes(table_name, name = nil) + indexes = [] + execute("EXEC sp_helpindex #{table_name}", name).each do |index| + unique = index[1] =~ /unique/ + primary = index[1] =~ /primary key/ + if !primary + indexes << IndexDefinition.new(table_name, index[0], unique, index[2].split(", ")) + end + end + indexes + end + + def rename_table(name, new_name) + execute "EXEC sp_rename '#{name}', '#{new_name}'" + end + + def remove_column(table_name, column_name) + execute "ALTER TABLE #{table_name} DROP COLUMN #{column_name}" + end + + def rename_column(table, column, new_column_name) + execute "EXEC sp_rename '#{table}.#{column}', '#{new_column_name}'" + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + sql_commands = ["ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit])}"] + if options[:default] + remove_default_constraint(table_name, column_name) + sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{options[:default]} FOR #{column_name}" + end + sql_commands.each {|c| + execute(c) + } + end + + def remove_column(table_name, column_name) + remove_default_constraint(table_name, column_name) + execute "ALTER TABLE #{table_name} DROP COLUMN #{column_name}" + end + + def remove_default_constraint(table_name, column_name) + defaults = select "select def.name from sysobjects def, syscolumns col, sysobjects tab where col.cdefault = def.id and col.name = '#{column_name}' and tab.name = '#{table_name}' and col.id = tab.id" + defaults.each {|constraint| + execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["name"]}" + } + end + + def remove_index(table_name, options = {}) + execute "DROP INDEX #{table_name}.#{index_name(table_name, options)}" + end + + def type_to_sql(type, limit = nil) #:nodoc: + native = native_database_types[type] + # if there's no :limit in the default type definition, assume that type doesn't support limits + limit = limit || native[:limit] + column_type_sql = native[:name] + column_type_sql << "(#{limit})" if limit + column_type_sql + end + + private + def select(sql, name = nil) + rows = [] + repair_special_columns(sql) + log(sql, name) do + @connection.select_all(sql) do |row| + record = {} + row.column_names.each do |col| + record[col] = row[col] + record[col] = record[col].to_time if record[col].is_a? DBI::Timestamp + end + rows << record + end + end + rows + end + + def enable_identity_insert(table_name, enable = true) + if has_identity_column(table_name) + "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}" + end + end + + def get_table_name(sql) + if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i + $1 + elsif sql =~ /from\s+([^\(\s]+)\s*/i + $1 + else + nil + end + end + + def has_identity_column(table_name) + !get_identity_column(table_name).nil? + end + + def get_identity_column(table_name) + @table_columns = {} unless @table_columns + @table_columns[table_name] = columns(table_name) if @table_columns[table_name] == nil + @table_columns[table_name].each do |col| + return col.name if col.identity + end + + return nil + end + + def query_contains_identity_column(sql, col) + sql =~ /\[#{col}\]/ + end + + def change_order_direction(order) + order.split(",").collect {|fragment| + case fragment + when /\bDESC\b/i then fragment.gsub(/\bDESC\b/i, "ASC") + when /\bASC\b/i then fragment.gsub(/\bASC\b/i, "DESC") + else String.new(fragment).split(',').join(' DESC,') + ' DESC' + end + }.join(",") + end + + def get_special_columns(table_name) + special = [] + @table_columns ||= {} + @table_columns[table_name] ||= columns(table_name) + @table_columns[table_name].each do |col| + special << col.name if col.is_special + end + special + end + + def repair_special_columns(sql) + special_cols = get_special_columns(get_table_name(sql)) + for col in special_cols.to_a + sql.gsub!(Regexp.new(" #{col.to_s} = "), " #{col.to_s} LIKE ") + sql.gsub!(/ORDER BY #{col.to_s}/i, '') + end + sql + end + + end #class SQLServerAdapter < AbstractAdapter + end #module ConnectionAdapters +end #module ActiveRecord diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/sybase_adapter.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/sybase_adapter.rb new file mode 100644 index 00000000..33dbebce --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/sybase_adapter.rb @@ -0,0 +1,684 @@ +# sybase_adaptor.rb +# Author: John Sheets +# Date: 01 Mar 2006 +# +# Based on code from Will Sobel (http://dev.rubyonrails.org/ticket/2030) +# +# 17 Mar 2006: Added support for migrations; fixed issues with :boolean columns. +# + +require 'active_record/connection_adapters/abstract_adapter' + +begin +require 'sybsql' + +module ActiveRecord + class Base + # Establishes a connection to the database that's used by all Active Record objects + def self.sybase_connection(config) # :nodoc: + config = config.symbolize_keys + + username = config[:username] ? config[:username].to_s : 'sa' + password = config[:password] ? config[:password].to_s : '' + + if config.has_key?(:host) + host = config[:host] + else + raise ArgumentError, "No database server name specified. Missing argument: host." + end + + if config.has_key?(:database) + database = config[:database] + else + raise ArgumentError, "No database specified. Missing argument: database." + end + + ConnectionAdapters::SybaseAdapter.new( + SybSQL.new({'S' => host, 'U' => username, 'P' => password}, + ConnectionAdapters::SybaseAdapterContext), database, logger) + end + end # class Base + + module ConnectionAdapters + + # ActiveRecord connection adapter for Sybase Open Client bindings + # (see http://raa.ruby-lang.org/project/sybase-ctlib). + # + # Options: + # + # * :host -- The name of the database server. No default, must be provided. + # * :database -- The name of the database. No default, must be provided. + # * :username -- Defaults to sa. + # * :password -- Defaults to empty string. + # + # Usage Notes: + # + # * The sybase-ctlib bindings do not support the DATE SQL column type; use DATETIME instead. + # * Table and column names are limited to 30 chars in Sybase 12.5 + # * :binary columns not yet supported + # * :boolean columns use the BIT SQL type, which does not allow nulls or + # indexes. If a DEFAULT is not specified for ALTER TABLE commands, the + # column will be declared with DEFAULT 0 (false). + # + # Migrations: + # + # The Sybase adapter supports migrations, but for ALTER TABLE commands to + # work, the database must have the database option 'select into' set to + # 'true' with sp_dboption (see below). The sp_helpdb command lists the current + # options for all databases. + # + # 1> use mydb + # 2> go + # 1> master..sp_dboption mydb, "select into", true + # 2> go + # 1> checkpoint + # 2> go + class SybaseAdapter < AbstractAdapter # :nodoc: + class ColumnWithIdentity < Column + attr_reader :identity, :primary + + def initialize(name, default, sql_type = nil, nullable = nil, identity = nil, primary = nil) + super(name, default, sql_type, nullable) + @default, @identity, @primary = type_cast(default), identity, primary + end + + def simplified_type(field_type) + case field_type + when /int|bigint|smallint|tinyint/i then :integer + when /float|double|decimal|money|numeric|real|smallmoney/i then :float + when /text|ntext/i then :text + when /binary|image|varbinary/i then :binary + when /char|nchar|nvarchar|string|varchar/i then :string + when /bit/i then :boolean + when /datetime|smalldatetime/i then :datetime + else super + end + end + + def self.string_to_binary(value) + "0x#{value.unpack("H*")[0]}" + end + + def self.binary_to_string(value) + # FIXME: sybase-ctlib uses separate sql method for binary columns. + value + end + end # class ColumnWithIdentity + + # Sybase adapter + def initialize(connection, database, logger = nil) + super(connection, logger) + context = connection.context + context.init(logger) + @limit = @offset = 0 + unless connection.sql_norow("USE #{database}") + raise "Cannot USE #{database}" + end + end + + def native_database_types + { + :primary_key => "numeric(9,0) IDENTITY PRIMARY KEY", + :string => { :name => "varchar", :limit => 255 }, + :text => { :name => "text" }, + :integer => { :name => "int" }, + :float => { :name => "float", :limit => 8 }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "timestamp" }, + :time => { :name => "time" }, + :date => { :name => "datetime" }, + :binary => { :name => "image"}, + :boolean => { :name => "bit" } + } + end + + def adapter_name + 'Sybase' + end + + def active? + !(@connection.connection.nil? || @connection.connection_dead?) + end + + def disconnect! + @connection.close rescue nil + end + + def reconnect! + raise "Sybase Connection Adapter does not yet support reconnect!" + # disconnect! + # connect! # Not yet implemented + end + + def table_alias_length + 30 + end + + # Check for a limit statement and parse out the limit and + # offset if specified. Remove the limit from the sql statement + # and call select. + def select_all(sql, name = nil) + select(sql, name) + end + + # Remove limit clause from statement. This will almost always + # contain LIMIT 1 from the caller. set the rowcount to 1 before + # calling select. + def select_one(sql, name = nil) + result = select(sql, name) + result.nil? ? nil : result.first + end + + def columns(table_name, name = nil) + table_structure(table_name).inject([]) do |columns, column| + name, default, type, nullable, identity, primary = column + columns << ColumnWithIdentity.new(name, default, type, nullable, identity, primary) + columns + end + end + + def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + begin + table_name = get_table_name(sql) + col = get_identity_column(table_name) + ii_enabled = false + + if col != nil + if query_contains_identity_column(sql, col) + begin + execute enable_identity_insert(table_name, true) + ii_enabled = true + rescue Exception => e + raise ActiveRecordError, "IDENTITY_INSERT could not be turned ON" + end + end + end + + log(sql, name) do + execute(sql, name) + ident = select_one("SELECT @@IDENTITY AS last_id")["last_id"] + id_value || ident + end + ensure + if ii_enabled + begin + execute enable_identity_insert(table_name, false) + rescue Exception => e + raise ActiveRecordError, "IDENTITY_INSERT could not be turned OFF" + end + end + end + end + + def execute(sql, name = nil) + log(sql, name) do + @connection.context.reset + @connection.set_rowcount(@limit || 0) + @limit = @offset = nil + @connection.sql_norow(sql) + if @connection.cmd_fail? or @connection.context.failed? + raise "SQL Command Failed for #{name}: #{sql}\nMessage: #{@connection.context.message}" + end + end + # Return rows affected + @connection.results[0].row_count + end + + alias_method :update, :execute + alias_method :delete, :execute + + def begin_db_transaction() execute "BEGIN TRAN" end + def commit_db_transaction() execute "COMMIT TRAN" end + def rollback_db_transaction() execute "ROLLBACK TRAN" end + + def tables(name = nil) + tables = [] + select("select name from sysobjects where type='U'", name).each do |row| + tables << row['name'] + end + tables + end + + def indexes(table_name, name = nil) + indexes = [] + select("exec sp_helpindex #{table_name}", name).each do |index| + unique = index["index_description"] =~ /unique/ + primary = index["index_description"] =~ /^clustered/ + if !primary + cols = index["index_keys"].split(", ").each { |col| col.strip! } + indexes << IndexDefinition.new(table_name, index["index_name"], unique, cols) + end + end + indexes + end + + def quoted_true + "1" + end + + def quoted_false + "0" + end + + def quote(value, column = nil) + case value + when String + if column && column.type == :binary && column.class.respond_to?(:string_to_binary) + "#{quote_string(column.class.string_to_binary(value))}" + elsif value =~ /^[+-]?[0-9]+$/o + value + else + "'#{quote_string(value)}'" + end + when NilClass then (column && column.type == :boolean) ? '0' : "NULL" + when TrueClass then '1' + when FalseClass then '0' + when Float, Fixnum, Bignum then value.to_s + when Date then "'#{value.to_s}'" + when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'" + else "'#{quote_string(value.to_yaml)}'" + end + end + + def quote_column(type, value) + case type + when :boolean + case value + when String then value =~ /^[ty]/o ? 1 : 0 + when true then 1 + when false then 0 + else value.to_i + end + when :integer then value.to_i + when :float then value.to_f + when :text, :string, :enum + case value + when String, Symbol, Fixnum, Float, Bignum, TrueClass, FalseClass + "'#{quote_string(value.to_s)}'" + else + "'#{quote_string(value.to_yaml)}'" + end + when :date, :datetime, :time + case value + when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'" + when Date then "'#{value.to_s}'" + else "'#{quote_string(value)}'" + end + else "'#{quote_string(value.to_yaml)}'" + end + end + + def quote_string(s) + s.gsub(/'/, "''") # ' (for ruby-mode) + end + + def quote_column_name(name) + "[#{name}]" + end + + def add_limit_offset!(sql, options) # :nodoc: + @limit = options[:limit] + @offset = options[:offset] + if !normal_select? + # Use temp table to hack offset with Sybase + sql.sub!(/ FROM /i, ' INTO #artemp FROM ') + elsif zero_limit? + # "SET ROWCOUNT 0" turns off limits, so we have + # to use a cheap trick. + if sql =~ /WHERE/i + sql.sub!(/WHERE/i, 'WHERE 1 = 2 AND ') + elsif sql =~ /ORDER\s+BY/i + sql.sub!(/ORDER\s+BY/i, 'WHERE 1 = 2 ORDER BY') + else + sql << 'WHERE 1 = 2' + end + end + end + + def supports_migrations? #:nodoc: + true + end + + def rename_table(name, new_name) + execute "EXEC sp_rename '#{name}', '#{new_name}'" + end + + def rename_column(table, column, new_column_name) + execute "EXEC sp_rename '#{table}.#{column}', '#{new_column_name}'" + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + sql_commands = ["ALTER TABLE #{table_name} MODIFY #{column_name} #{type_to_sql(type, options[:limit])}"] + if options[:default] + remove_default_constraint(table_name, column_name) + sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{options[:default]} FOR #{column_name}" + end + sql_commands.each { |c| execute(c) } + end + + def remove_column(table_name, column_name) + remove_default_constraint(table_name, column_name) + execute "ALTER TABLE #{table_name} DROP #{column_name}" + end + + def remove_default_constraint(table_name, column_name) + defaults = select "select def.name from sysobjects def, syscolumns col, sysobjects tab where col.cdefault = def.id and col.name = '#{column_name}' and tab.name = '#{table_name}' and col.id = tab.id" + defaults.each {|constraint| + execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["name"]}" + } + end + + def remove_index(table_name, options = {}) + execute "DROP INDEX #{table_name}.#{index_name(table_name, options)}" + end + + def add_column_options!(sql, options) #:nodoc: + sql << " DEFAULT #{quote(options[:default], options[:column])}" unless options[:default].nil? + + if check_null_for_column?(options[:column], sql) + sql << (options[:null] == false ? " NOT NULL" : " NULL") + end + sql + end + + private + def check_null_for_column?(col, sql) + # Sybase columns are NOT NULL by default, so explicitly set NULL + # if :null option is omitted. Disallow NULLs for boolean. + type = col.nil? ? "" : col[:type] + + # Ignore :null if a primary key + return false if type =~ /PRIMARY KEY/i + + # Ignore :null if a :boolean or BIT column + if (sql =~ /\s+bit(\s+DEFAULT)?/i) || type == :boolean + # If no default clause found on a boolean column, add one. + sql << " DEFAULT 0" if $1.nil? + return false + end + true + end + + # Return the last value of the identity global value. + def last_insert_id + @connection.sql("SELECT @@IDENTITY") + unless @connection.cmd_fail? + id = @connection.top_row_result.rows.first.first + if id + id = id.to_i + id = nil if id == 0 + end + else + id = nil + end + id + end + + def affected_rows(name = nil) + @connection.sql("SELECT @@ROWCOUNT") + unless @connection.cmd_fail? + count = @connection.top_row_result.rows.first.first + count = count.to_i if count + else + 0 + end + end + + def normal_select? + # If limit is not set at all, we can ignore offset; + # If limit *is* set but offset is zero, use normal select + # with simple SET ROWCOUNT. Thus, only use the temp table + # if limit is set and offset > 0. + has_limit = !@limit.nil? + has_offset = !@offset.nil? && @offset > 0 + !has_limit || !has_offset + end + + def zero_limit? + !@limit.nil? && @limit == 0 + end + + # Select limit number of rows starting at optional offset. + def select(sql, name = nil) + @connection.context.reset + log(sql, name) do + if normal_select? + # If limit is not explicitly set, return all results. + @logger.debug "Setting row count to (#{@limit || 'off'})" if @logger + + # Run a normal select + @connection.set_rowcount(@limit || 0) + @connection.sql(sql) + else + # Select into a temp table and prune results + @logger.debug "Selecting #{@limit + (@offset || 0)} or fewer rows into #artemp" if @logger + @connection.set_rowcount(@limit + (@offset || 0)) + @connection.sql_norow(sql) # Select into temp table + @logger.debug "Deleting #{@offset || 0} or fewer rows from #artemp" if @logger + @connection.set_rowcount(@offset || 0) + @connection.sql_norow("delete from #artemp") # Delete leading rows + @connection.set_rowcount(0) + @connection.sql("select * from #artemp") # Return the rest + end + end + + rows = [] + if @connection.context.failed? or @connection.cmd_fail? + raise StatementInvalid, "SQL Command Failed for #{name}: #{sql}\nMessage: #{@connection.context.message}" + else + results = @connection.top_row_result + if results && results.rows.length > 0 + fields = fixup_column_names(results.columns) + results.rows.each do |row| + hashed_row = {} + row.zip(fields) { |cell, column| hashed_row[column] = cell } + rows << hashed_row + end + end + end + @connection.sql_norow("drop table #artemp") if !normal_select? + @limit = @offset = nil + return rows + end + + def enable_identity_insert(table_name, enable = true) + if has_identity_column(table_name) + "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}" + end + end + + def get_table_name(sql) + if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i + $1 + elsif sql =~ /from\s+([^\(\s]+)\s*/i + $1 + else + nil + end + end + + def has_identity_column(table_name) + !get_identity_column(table_name).nil? + end + + def get_identity_column(table_name) + @table_columns = {} unless @table_columns + @table_columns[table_name] = columns(table_name) if @table_columns[table_name] == nil + @table_columns[table_name].each do |col| + return col.name if col.identity + end + + return nil + end + + def query_contains_identity_column(sql, col) + sql =~ /\[#{col}\]/ + end + + # Remove trailing _ from names. + def fixup_column_names(columns) + columns.map { |column| column.sub(/_$/, '') } + end + + def table_structure(table_name) + sql = <= 128 + primary = (sysstat2 & 8) == 8 + + columns << [name, default_value, type, nullable, identity, primary] + end + columns + else + nil + end + end + + def normalize_type(field_type, prec, scale, length) + if field_type =~ /numeric/i and (scale.nil? or scale == 0) + type = 'int' + elsif field_type =~ /money/i + type = 'numeric' + else + type = field_type + end + size = '' + if prec + size = "(#{prec})" + elsif length + size = "(#{length})" + end + return type + size + end + + def default_value(value) + end + end # class SybaseAdapter + + class SybaseAdapterContext < SybSQLContext + DEADLOCK = 1205 + attr_reader :message + + def init(logger = nil) + @deadlocked = false + @failed = false + @logger = logger + @message = nil + end + + def srvmsgCB(con, msg) + # Do not log change of context messages. + if msg['severity'] == 10 or msg['severity'] == 0 + return true + end + + if msg['msgnumber'] == DEADLOCK + @deadlocked = true + else + @logger.info "SQL Command failed!" if @logger + @failed = true + end + + if @logger + @logger.error "** SybSQLContext Server Message: **" + @logger.error " Message number #{msg['msgnumber']} Severity #{msg['severity']} State #{msg['state']} Line #{msg['line']}" + @logger.error " Server #{msg['srvname']}" + @logger.error " Procedure #{msg['proc']}" + @logger.error " Message String: #{msg['text']}" + end + + @message = msg['text'] + + true + end + + def deadlocked? + @deadlocked + end + + def failed? + @failed + end + + def reset + @deadlocked = false + @failed = false + @message = nil + end + + def cltmsgCB(con, msg) + return true unless ( msg.kind_of?(Hash) ) + unless ( msg[ "severity" ] ) then + return true + end + + if @logger + @logger.error "** SybSQLContext Client-Message: **" + @logger.error " Message number: LAYER=#{msg[ 'layer' ]} ORIGIN=#{msg[ 'origin' ]} SEVERITY=#{msg[ 'severity' ]} NUMBER=#{msg[ 'number' ]}" + @logger.error " Message String: #{msg['msgstring']}" + @logger.error " OS Error: #{msg['osstring']}" + + @message = msg['msgstring'] + end + + @failed = true + + # Not retry , CS_CV_RETRY_FAIL( probability TimeOut ) + if( msg[ 'severity' ] == "RETRY_FAIL" ) then + @timeout_p = true + return false + end + + return true + end + end # class SybaseAdapterContext + + end # module ConnectionAdapters +end # module ActiveRecord + + +# Allow identity inserts for fixtures. +require "active_record/fixtures" +class Fixtures + alias :original_insert_fixtures :insert_fixtures + + def insert_fixtures + values.each do |fixture| + allow_identity_inserts table_name, true + @connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert' + allow_identity_inserts table_name, false + end + end + + def allow_identity_inserts(table_name, enable) + @connection.execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}" rescue nil + end +end + +rescue LoadError => cannot_require_sybase + # Couldn't load sybase adapter +end \ No newline at end of file diff --git a/vendor/rails/activerecord/lib/active_record/deprecated_associations.rb b/vendor/rails/activerecord/lib/active_record/deprecated_associations.rb new file mode 100644 index 00000000..077ac1de --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/deprecated_associations.rb @@ -0,0 +1,90 @@ +module ActiveRecord + module Associations # :nodoc: + module ClassMethods + def deprecated_collection_count_method(collection_name)# :nodoc: + module_eval <<-"end_eval", __FILE__, __LINE__ + def #{collection_name}_count(force_reload = false) + #{collection_name}.reload if force_reload + #{collection_name}.size + end + end_eval + end + + def deprecated_add_association_relation(association_name)# :nodoc: + module_eval <<-"end_eval", __FILE__, __LINE__ + def add_#{association_name}(*items) + #{association_name}.concat(items) + end + end_eval + end + + def deprecated_remove_association_relation(association_name)# :nodoc: + module_eval <<-"end_eval", __FILE__, __LINE__ + def remove_#{association_name}(*items) + #{association_name}.delete(items) + end + end_eval + end + + def deprecated_has_collection_method(collection_name)# :nodoc: + module_eval <<-"end_eval", __FILE__, __LINE__ + def has_#{collection_name}?(force_reload = false) + !#{collection_name}(force_reload).empty? + end + end_eval + end + + def deprecated_find_in_collection_method(collection_name)# :nodoc: + module_eval <<-"end_eval", __FILE__, __LINE__ + def find_in_#{collection_name}(association_id) + #{collection_name}.find(association_id) + end + end_eval + end + + def deprecated_find_all_in_collection_method(collection_name)# :nodoc: + module_eval <<-"end_eval", __FILE__, __LINE__ + def find_all_in_#{collection_name}(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil) + #{collection_name}.find_all(runtime_conditions, orderings, limit, joins) + end + end_eval + end + + def deprecated_collection_create_method(collection_name)# :nodoc: + module_eval <<-"end_eval", __FILE__, __LINE__ + def create_in_#{collection_name}(attributes = {}) + #{collection_name}.create(attributes) + end + end_eval + end + + def deprecated_collection_build_method(collection_name)# :nodoc: + module_eval <<-"end_eval", __FILE__, __LINE__ + def build_to_#{collection_name}(attributes = {}) + #{collection_name}.build(attributes) + end + end_eval + end + + def deprecated_association_comparison_method(association_name, association_class_name) # :nodoc: + module_eval <<-"end_eval", __FILE__, __LINE__ + def #{association_name}?(comparison_object, force_reload = false) + if comparison_object.kind_of?(#{association_class_name}) + #{association_name}(force_reload) == comparison_object + else + raise "Comparison object is a #{association_class_name}, should have been \#{comparison_object.class.name}" + end + end + end_eval + end + + def deprecated_has_association_method(association_name) # :nodoc: + module_eval <<-"end_eval", __FILE__, __LINE__ + def has_#{association_name}?(force_reload = false) + !#{association_name}(force_reload).nil? + end + end_eval + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/deprecated_finders.rb b/vendor/rails/activerecord/lib/active_record/deprecated_finders.rb new file mode 100644 index 00000000..6f56bf55 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/deprecated_finders.rb @@ -0,0 +1,41 @@ +module ActiveRecord + class Base + class << self + # 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+. + # +RecordNotFound+ is raised if no record can be found matching the +id+ or meeting the condition. + # Example: + # Person.find_on_conditions 5, "first_name LIKE '%dav%' AND last_name = 'heinemeier'" + def find_on_conditions(ids, conditions) # :nodoc: + find(ids, :conditions => conditions) + end + + # This method is deprecated in favor of find(:first, options). + # + # 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 + # be used to create the object. In such cases, it might be beneficial to also specify + # +orderings+, like "income DESC, name", to control exactly which record is to be used. Example: + # Employee.find_first "income > 50000", "income DESC, name" + def find_first(conditions = nil, orderings = nil, joins = nil) # :nodoc: + find(:first, :conditions => conditions, :order => orderings, :joins => joins) + end + + # This method is deprecated in favor of find(:all, options). + # + # 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), + # such as by "color = 'red'", and arrangement of the selection can be done through +orderings+ (ORDER BY-part), + # such as by "last_name, first_name DESC". A maximum of returned objects and their offset can be specified in + # +limit+ with either just a single integer as the limit or as an array with the first element as the limit, + # the second as the offset. Examples: + # Project.find_all "category = 'accounts'", "last_accessed DESC", 15 + # Project.find_all ["category = ?", category_name], "created ASC", [15, 20] + def find_all(conditions = nil, orderings = nil, limit = nil, joins = nil) # :nodoc: + limit, offset = limit.is_a?(Array) ? limit : [ limit, nil ] + find(:all, :conditions => conditions, :order => orderings, :joins => joins, :limit => limit, :offset => offset) + end + end + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/lib/active_record/fixtures.rb b/vendor/rails/activerecord/lib/active_record/fixtures.rb new file mode 100755 index 00000000..f83219f5 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/fixtures.rb @@ -0,0 +1,600 @@ +require 'erb' +require 'yaml' +require 'csv' + +module YAML #:nodoc: + class Omap #:nodoc: + def keys; map { |k, v| k } end + def values; map { |k, v| v } end + end +end + +class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc: +end + +# Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavours: +# +# 1. YAML fixtures +# 2. CSV fixtures +# 3. Single-file fixtures +# +# = YAML fixtures +# +# This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures +# in a non-verbose, humanly-readable format. It ships with Ruby 1.8.1+. +# +# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed in the directory appointed +# by Test::Unit::TestCase.fixture_path=(path) (this is automatically configured for Rails, so you can just +# put your files in /test/fixtures/). The fixture file ends with the .yml file extension (Rails example: +# "/test/fixtures/web_sites.yml"). The format of a YAML fixture file looks like this: +# +# rubyonrails: +# id: 1 +# name: Ruby on Rails +# url: http://www.rubyonrails.org +# +# google: +# id: 2 +# name: Google +# url: http://www.google.com +# +# This YAML fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and is followed by an +# indented list of key/value pairs in the "key: value" format. Records are separated by a blank line for your viewing +# pleasure. +# +# Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type. See http://yaml.org/type/omap.html +# for the specification. You will need ordered fixtures when you have foreign key constraints on keys in the same table. +# This is commonly needed for tree structures. Example: +# +# --- !omap +# - parent: +# id: 1 +# parent_id: NULL +# title: Parent +# - child: +# id: 2 +# parent_id: 1 +# title: Child +# +# = CSV fixtures +# +# Fixtures can also be kept in the Comma Separated Value format. Akin to YAML fixtures, CSV fixtures are stored +# in a single file, but instead end with the .csv file extension (Rails example: "/test/fixtures/web_sites.csv") +# +# The format of this type of fixture file is much more compact than the others, but also a little harder to read by us +# humans. The first line of the CSV file is a comma-separated list of field names. The rest of the file is then comprised +# of the actual data (1 per line). Here's an example: +# +# id, name, url +# 1, Ruby On Rails, http://www.rubyonrails.org +# 2, Google, http://www.google.com +# +# Should you have a piece of data with a comma character in it, you can place double quotes around that value. If you +# need to use a double quote character, you must escape it with another double quote. +# +# Another unique attribute of the CSV fixture is that it has *no* fixture name like the other two formats. Instead, the +# fixture names are automatically generated by deriving the class name of the fixture file and adding an incrementing +# number to the end. In our example, the 1st fixture would be called "web_site_1" and the 2nd one would be called +# "web_site_2". +# +# Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you +# have existing data somewhere already. +# +# = Single-file fixtures +# +# This type of fixtures was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats. +# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory +# appointed by Test::Unit::TestCase.fixture_path=(path) (this is automatically configured for Rails, so you can just +# put your files in /test/fixtures// -- like /test/fixtures/web_sites/ for the WebSite +# model). +# +# Each text file placed in this directory represents a "record". Usually these types of fixtures are named without +# extensions, but if you are on a Windows machine, you might consider adding .txt as the extension. Here's what the +# above example might look like: +# +# web_sites/google +# web_sites/yahoo.txt +# web_sites/ruby-on-rails +# +# The file format of a standard fixture is simple. Each line is a property (or column in db speak) and has the syntax +# of "name => value". Here's an example of the ruby-on-rails fixture above: +# +# id => 1 +# name => Ruby on Rails +# url => http://www.rubyonrails.org +# +# = Using Fixtures +# +# Since fixtures are a testing construct, we use them in our unit and functional tests. There are two ways to use the +# fixtures, but first let's take a look at a sample unit test found: +# +# require 'web_site' +# +# class WebSiteTest < Test::Unit::TestCase +# def test_web_site_count +# assert_equal 2, WebSite.count +# end +# end +# +# As it stands, unless we pre-load the web_site table in our database with two records, this test will fail. Here's the +# easiest way to add fixtures to the database: +# +# ... +# class WebSiteTest < Test::Unit::TestCase +# fixtures :web_sites # add more by separating the symbols with commas +# ... +# +# By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here tho), we trigger +# the testing environment to automatically load the appropriate fixtures into the database before each test. +# To ensure consistent data, the environment deletes the fixtures before running the load. +# +# In addition to being available in the database, the fixtures are also loaded into a hash stored in an instance variable +# of the test case. It is named after the symbol... so, in our example, there would be a hash available called +# @web_sites. This is where the "fixture name" comes into play. +# +# On top of that, each record is automatically "found" (using Model.find(id)) and placed in the instance variable of its name. +# So for the YAML fixtures, we'd get @rubyonrails and @google, which could be interrogated using regular Active Record semantics: +# +# # test if the object created from the fixture data has the same attributes as the data itself +# def test_find +# assert_equal @web_sites["rubyonrails"]["name"], @rubyonrails.name +# end +# +# As seen above, the data hash created from the YAML fixtures would have @web_sites["rubyonrails"]["url"] return +# "http://www.rubyonrails.org" and @web_sites["google"]["name"] would return "Google". The same fixtures, but loaded +# from a CSV fixture file, would be accessible via @web_sites["web_site_1"]["name"] == "Ruby on Rails" and have the individual +# fixtures available as instance variables @web_site_1 and @web_site_2. +# +# If you do not wish to use instantiated fixtures (usually for performance reasons) there are two options. +# +# - to completely disable instantiated fixtures: +# self.use_instantiated_fixtures = false +# +# - to keep the fixture instance (@web_sites) available, but do not automatically 'find' each instance: +# self.use_instantiated_fixtures = :no_instances +# +# Even if auto-instantiated fixtures are disabled, you can still access them +# by name via special dynamic methods. Each method has the same name as the +# model, and accepts the name of the fixture to instantiate: +# +# fixtures :web_sites +# +# def test_find +# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name +# end +# +# = Dynamic fixtures with ERb +# +# Some times you don't care about the content of the fixtures as much as you care about the volume. In these cases, you can +# mix ERb in with your YAML or CSV fixtures to create a bunch of fixtures for load testing, like: +# +# <% for i in 1..1000 %> +# fix_<%= i %>: +# id: <%= i %> +# name: guy_<%= 1 %> +# <% end %> +# +# This will create 1000 very simple YAML fixtures. +# +# Using ERb, you can also inject dynamic values into your fixtures with inserts like <%= Date.today.strftime("%Y-%m-%d") %>. +# This is however a feature to be used with some caution. The point of fixtures are that they're stable units of predictable +# sample data. If you feel that you need to inject dynamic values, then perhaps you should reexamine whether your application +# is properly testable. Hence, dynamic values in fixtures are to be considered a code smell. +# +# = Transactional fixtures +# +# TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case. +# They can also turn off auto-instantiation of fixture data since the feature is costly and often unused. +# +# class FooTest < Test::Unit::TestCase +# self.use_transactional_fixtures = true +# self.use_instantiated_fixtures = false +# +# fixtures :foos +# +# def test_godzilla +# assert !Foo.find(:all).empty? +# Foo.destroy_all +# assert Foo.find(:all).empty? +# end +# +# def test_godzilla_aftermath +# assert !Foo.find(:all).empty? +# end +# end +# +# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures, +# then you may omit all fixtures declarations in your test cases since all the data's already there and every case rolls back its changes. +# +# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide +# access to fixture data for every table that has been loaded through fixtures (depending on the value of +use_instantiated_fixtures+) +# +# When *not* to use transactional fixtures: +# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit, +# particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify +# the results of your transaction until Active Record supports nested transactions or savepoints (in progress.) +# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM. +# Use InnoDB, MaxDB, or NDB instead. +class Fixtures < YAML::Omap + DEFAULT_FILTER_RE = /\.ya?ml$/ + + def self.instantiate_fixtures(object, table_name, fixtures, load_instances=true) + object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures + if load_instances + ActiveRecord::Base.silence do + fixtures.each do |name, fixture| + begin + object.instance_variable_set "@#{name}", fixture.find + rescue FixtureClassNotFound + nil + end + end + end + end + end + + def self.instantiate_all_loaded_fixtures(object, load_instances=true) + all_loaded_fixtures.each do |table_name, fixtures| + Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances) + end + end + + cattr_accessor :all_loaded_fixtures + self.all_loaded_fixtures = {} + + def self.create_fixtures(fixtures_directory, table_names, class_names = {}) + table_names = [table_names].flatten.map { |n| n.to_s } + connection = block_given? ? yield : ActiveRecord::Base.connection + ActiveRecord::Base.silence do + fixtures_map = {} + fixtures = table_names.map do |table_name| + fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s)) + end + all_loaded_fixtures.merge! fixtures_map + + connection.transaction do + fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } + fixtures.each { |fixture| fixture.insert_fixtures } + + # Cap primary key sequences to max(pk). + if connection.respond_to?(:reset_pk_sequence!) + table_names.each do |table_name| + connection.reset_pk_sequence!(table_name) + end + end + end + + return fixtures.size > 1 ? fixtures : fixtures.first + end + end + + + attr_reader :table_name + + def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE) + @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter + @class_name = class_name || + (ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize) + @table_name = ActiveRecord::Base.table_name_prefix + @table_name + ActiveRecord::Base.table_name_suffix + read_fixture_files + end + + def delete_existing_fixtures + @connection.delete "DELETE FROM #{@table_name}", 'Fixture Delete' + end + + def insert_fixtures + values.each do |fixture| + @connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert' + end + end + + private + + def read_fixture_files + if File.file?(yaml_file_path) + # YAML fixtures + begin + yaml_string = "" + Dir["#{@fixture_path}/**/*.yml"].select {|f| test(?f,f) }.each do |subfixture_path| + yaml_string << IO.read(subfixture_path) + end + yaml_string << IO.read(yaml_file_path) + + if yaml = YAML::load(erb_render(yaml_string)) + yaml = yaml.value if yaml.respond_to?(:type_id) and yaml.respond_to?(:value) + yaml.each do |name, data| + self[name] = Fixture.new(data, @class_name) + end + end + rescue Exception=>boom + raise Fixture::FormatError, "a YAML error occured parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{boom.class}: #{boom}" + end + elsif File.file?(csv_file_path) + # CSV fixtures + reader = CSV::Reader.create(erb_render(IO.read(csv_file_path))) + header = reader.shift + i = 0 + reader.each do |row| + data = {} + row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip } + self["#{Inflector::underscore(@class_name)}_#{i+=1}"]= Fixture.new(data, @class_name) + end + elsif File.file?(deprecated_yaml_file_path) + raise Fixture::FormatError, ".yml extension required: rename #{deprecated_yaml_file_path} to #{yaml_file_path}" + else + # Standard fixtures + Dir.entries(@fixture_path).each do |file| + path = File.join(@fixture_path, file) + if File.file?(path) and file !~ @file_filter + self[file] = Fixture.new(path, @class_name) + end + end + end + end + + def yaml_file_path + "#{@fixture_path}.yml" + end + + def deprecated_yaml_file_path + "#{@fixture_path}.yaml" + end + + def csv_file_path + @fixture_path + ".csv" + end + + def yaml_fixtures_key(path) + File.basename(@fixture_path).split(".").first + end + + def erb_render(fixture_content) + ERB.new(fixture_content).result + end +end + +class Fixture #:nodoc: + include Enumerable + class FixtureError < StandardError#:nodoc: + end + class FormatError < FixtureError#:nodoc: + end + + def initialize(fixture, class_name) + case fixture + when Hash, YAML::Omap + @fixture = fixture + when String + @fixture = read_fixture_file(fixture) + else + raise ArgumentError, "Bad fixture argument #{fixture.inspect}" + end + + @class_name = class_name + end + + def each + @fixture.each { |item| yield item } + end + + def [](key) + @fixture[key] + end + + def to_hash + @fixture + end + + def key_list + columns = @fixture.keys.collect{ |column_name| ActiveRecord::Base.connection.quote_column_name(column_name) } + columns.join(", ") + end + + def value_list + @fixture.values.map { |v| ActiveRecord::Base.connection.quote(v).gsub('\\n', "\n").gsub('\\r', "\r") }.join(", ") + end + + def find + klass = @class_name.is_a?(Class) ? @class_name : Object.const_get(@class_name) rescue nil + if klass + klass.find(self[klass.primary_key]) + else + raise FixtureClassNotFound, "The class #{@class_name.inspect} was not found." + end + end + + private + def read_fixture_file(fixture_file_path) + IO.readlines(fixture_file_path).inject({}) do |fixture, line| + # Mercifully skip empty lines. + next if line =~ /^\s*$/ + + # Use the same regular expression for attributes as Active Record. + unless md = /^\s*([a-zA-Z][-_\w]*)\s*=>\s*(.+)\s*$/.match(line) + raise FormatError, "#{fixture_file_path}: fixture format error at '#{line}'. Expecting 'key => value'." + end + key, value = md.captures + + # Disallow duplicate keys to catch typos. + raise FormatError, "#{fixture_file_path}: duplicate '#{key}' in fixture." if fixture[key] + fixture[key] = value.strip + fixture + end + end +end + +module Test #:nodoc: + module Unit #:nodoc: + class TestCase #:nodoc: + cattr_accessor :fixture_path + class_inheritable_accessor :fixture_table_names + class_inheritable_accessor :fixture_class_names + class_inheritable_accessor :use_transactional_fixtures + class_inheritable_accessor :use_instantiated_fixtures # true, false, or :no_instances + class_inheritable_accessor :pre_loaded_fixtures + + self.fixture_table_names = [] + self.use_transactional_fixtures = false + self.use_instantiated_fixtures = true + self.pre_loaded_fixtures = false + + self.fixture_class_names = {} + + @@already_loaded_fixtures = {} + self.fixture_class_names = {} + + def self.set_fixture_class(class_names = {}) + self.fixture_class_names = self.fixture_class_names.merge(class_names) + end + + def self.fixtures(*table_names) + table_names = table_names.flatten.map { |n| n.to_s } + self.fixture_table_names |= table_names + require_fixture_classes(table_names) + setup_fixture_accessors(table_names) + end + + def self.require_fixture_classes(table_names=nil) + (table_names || fixture_table_names).each do |table_name| + file_name = table_name.to_s + file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names + begin + require file_name + rescue LoadError + # Let's hope the developer has included it himself + end + end + end + + def self.setup_fixture_accessors(table_names=nil) + (table_names || fixture_table_names).each do |table_name| + table_name = table_name.to_s.tr('.','_') + define_method(table_name) do |fixture, *optionals| + force_reload = optionals.shift + @fixture_cache[table_name] ||= Hash.new + @fixture_cache[table_name][fixture] = nil if force_reload + if @loaded_fixtures[table_name][fixture.to_s] + @fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find + else + raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'" + end + end + end + end + + def self.uses_transaction(*methods) + @uses_transaction ||= [] + @uses_transaction.concat methods.map { |m| m.to_s } + end + + def self.uses_transaction?(method) + @uses_transaction && @uses_transaction.include?(method.to_s) + end + + def use_transactional_fixtures? + use_transactional_fixtures && + !self.class.uses_transaction?(method_name) + end + + def setup_with_fixtures + if pre_loaded_fixtures && !use_transactional_fixtures + raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' + end + + @fixture_cache = Hash.new + + # Load fixtures once and begin transaction. + if use_transactional_fixtures? + if @@already_loaded_fixtures[self.class] + @loaded_fixtures = @@already_loaded_fixtures[self.class] + else + load_fixtures + @@already_loaded_fixtures[self.class] = @loaded_fixtures + end + ActiveRecord::Base.lock_mutex + ActiveRecord::Base.connection.begin_db_transaction + + # Load fixtures for every test. + else + @@already_loaded_fixtures[self.class] = nil + load_fixtures + end + + # Instantiate fixtures for every test if requested. + instantiate_fixtures if use_instantiated_fixtures + end + + alias_method :setup, :setup_with_fixtures + + def teardown_with_fixtures + # Rollback changes. + if use_transactional_fixtures? + ActiveRecord::Base.connection.rollback_db_transaction + ActiveRecord::Base.unlock_mutex + end + ActiveRecord::Base.verify_active_connections! + end + + alias_method :teardown, :teardown_with_fixtures + + def self.method_added(method) + case method.to_s + when 'setup' + unless method_defined?(:setup_without_fixtures) + alias_method :setup_without_fixtures, :setup + define_method(:setup) do + setup_with_fixtures + setup_without_fixtures + end + end + when 'teardown' + unless method_defined?(:teardown_without_fixtures) + alias_method :teardown_without_fixtures, :teardown + define_method(:teardown) do + teardown_without_fixtures + teardown_with_fixtures + end + end + end + end + + private + def load_fixtures + @loaded_fixtures = {} + fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) + unless fixtures.nil? + if fixtures.instance_of?(Fixtures) + @loaded_fixtures[fixtures.table_name] = fixtures + else + fixtures.each { |f| @loaded_fixtures[f.table_name] = f } + end + end + end + + # for pre_loaded_fixtures, only require the classes once. huge speed improvement + @@required_fixture_classes = false + + def instantiate_fixtures + if pre_loaded_fixtures + raise RuntimeError, 'Load fixtures before instantiating them.' if Fixtures.all_loaded_fixtures.empty? + unless @@required_fixture_classes + self.class.require_fixture_classes Fixtures.all_loaded_fixtures.keys + @@required_fixture_classes = true + end + Fixtures.instantiate_all_loaded_fixtures(self, load_instances?) + else + raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil? + @loaded_fixtures.each do |table_name, fixtures| + Fixtures.instantiate_fixtures(self, table_name, fixtures, load_instances?) + end + end + end + + def load_instances? + use_instantiated_fixtures != :no_instances + end + end + + end +end diff --git a/vendor/rails/activerecord/lib/active_record/locking.rb b/vendor/rails/activerecord/lib/active_record/locking.rb new file mode 100644 index 00000000..ca83a98b --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/locking.rb @@ -0,0 +1,79 @@ +module ActiveRecord + # Active Records support optimistic locking if the field lock_version is present. Each update to the + # record increments the lock_version column and the locking facilities ensure that records instantiated twice + # will let the last one saved raise a StaleObjectError if the first was also updated. Example: + # + # p1 = Person.find(1) + # p2 = Person.find(1) + # + # p1.first_name = "Michael" + # p1.save + # + # p2.first_name = "should fail" + # p2.save # Raises a ActiveRecord::StaleObjectError + # + # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, + # or otherwise apply the business logic needed to resolve the conflict. + # + # You must ensure that your database schema defaults the lock_version column to 0. + # + # This behavior can be turned off by setting ActiveRecord::Base.lock_optimistically = false. + # To override the name of the lock_version column, invoke the set_locking_column method. + # This method uses the same syntax as set_table_name + module Locking + def self.append_features(base) #:nodoc: + super + base.class_eval do + alias_method :update_without_lock, :update + alias_method :update, :update_with_lock + end + end + + def update_with_lock #:nodoc: + return update_without_lock unless locking_enabled? + + lock_col = self.class.locking_column + previous_value = send(lock_col) + send(lock_col + '=', previous_value + 1) + + affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking") + UPDATE #{self.class.table_name} + SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} + WHERE #{self.class.primary_key} = #{quote(id)} + AND #{lock_col} = #{quote(previous_value)} + end_sql + + unless affected_rows == 1 + raise ActiveRecord::StaleObjectError, "Attempted to update a stale object" + end + + return true + end + end + + class Base + @@lock_optimistically = true + cattr_accessor :lock_optimistically + + def locking_enabled? #:nodoc: + lock_optimistically && respond_to?(self.class.locking_column) + end + + class << self + def set_locking_column(value = nil, &block) + define_attr_method :locking_column, value, &block + end + + def locking_column #:nodoc: + reset_locking_column + end + + def reset_locking_column #:nodoc: + default = 'lock_version' + set_locking_column(default) + default + end + end + + end +end diff --git a/vendor/rails/activerecord/lib/active_record/migration.rb b/vendor/rails/activerecord/lib/active_record/migration.rb new file mode 100644 index 00000000..a1934d40 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/migration.rb @@ -0,0 +1,391 @@ +module ActiveRecord + class IrreversibleMigration < ActiveRecordError#:nodoc: + end + + class DuplicateMigrationVersionError < ActiveRecordError#:nodoc: + def initialize(version) + super("Multiple migrations have the version number #{version}") + end + end + + # Migrations can manage the evolution of a schema used by several physical databases. It's a solution + # to the common problem of adding a field to make a new feature work in your local database, but being unsure of how to + # push that change to other developers and to the production server. With migrations, you can describe the transformations + # in self-contained classes that can be checked into version control systems and executed against another database that + # might be one, two, or five versions behind. + # + # Example of a simple migration: + # + # class AddSsl < ActiveRecord::Migration + # def self.up + # add_column :accounts, :ssl_enabled, :boolean, :default => 1 + # end + # + # def self.down + # remove_column :accounts, :ssl_enabled + # end + # end + # + # This migration will add a boolean flag to the accounts table and remove it again, if you're backing out of the migration. + # It shows how all migrations have two class methods +up+ and +down+ that describes the transformations required to implement + # or remove the migration. These methods can consist of both the migration specific methods, like add_column and remove_column, + # but may also contain regular Ruby code for generating data needed for the transformations. + # + # Example of a more complex migration that also needs to initialize data: + # + # class AddSystemSettings < ActiveRecord::Migration + # def self.up + # create_table :system_settings do |t| + # t.column :name, :string + # t.column :label, :string + # t.column :value, :text + # t.column :type, :string + # t.column :position, :integer + # end + # + # SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1 + # end + # + # def self.down + # drop_table :system_settings + # end + # end + # + # This migration first adds the system_settings table, then creates the very first row in it using the Active Record model + # that relies on the table. It also uses the more advanced create_table syntax where you can specify a complete table schema + # in one block call. + # + # == Available transformations + # + # * create_table(name, options) Creates a table called +name+ and makes the table object available to a block + # that can then add columns to it, following the same format as add_column. See example above. The options hash is for + # fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create table definition. + # * drop_table(name): Drops the table called +name+. + # * rename_table(old_name, new_name): Renames the table called +old_name+ to +new_name+. + # * add_column(table_name, column_name, type, options): Adds a new column to the table called +table_name+ + # named +column_name+ specified to be one of the following types: + # :string, :text, :integer, :float, :datetime, :timestamp, :time, :date, :binary, :boolean. A default value can be specified + # by passing an +options+ hash like { :default => 11 }. + # * rename_column(table_name, column_name, new_column_name): Renames a column but keeps the type and content. + # * change_column(table_name, column_name, type, options): Changes the column to a different type using the same + # parameters as add_column. + # * remove_column(table_name, column_name): Removes the column named +column_name+ from the table called +table_name+. + # * add_index(table_name, column_names, index_type, index_name): Add a new index with the name of the column, or +index_name+ (if specified) on the column(s). Specify an optional +index_type+ (e.g. UNIQUE). + # * remove_index(table_name, index_name): Remove the index specified by +index_name+. + # + # == Irreversible transformations + # + # Some transformations are destructive in a manner that cannot be reversed. Migrations of that kind should raise + # an IrreversibleMigration exception in their +down+ method. + # + # == Running migrations from within Rails + # + # The Rails package has several tools to help create and apply migrations. + # + # To generate a new migration, use script/generate migration MyNewMigration + # where MyNewMigration is the name of your migration. The generator will + # create a file nnn_my_new_migration.rb in the db/migrate/ + # directory, where nnn is the next largest migration number. + # You may then edit the self.up and self.down methods of + # n MyNewMigration. + # + # To run migrations against the currently configured database, use + # rake migrate. This will update the database by running all of the + # pending migrations, creating the schema_info table if missing. + # + # To roll the database back to a previous migration version, use + # rake migrate VERSION=X where X is the version to which + # you wish to downgrade. If any of the migrations throw an + # IrreversibleMigration exception, that step will fail and you'll + # have some manual work to do. + # + # == Database support + # + # Migrations are currently supported in MySQL, PostgreSQL, SQLite, + # SQL Server, Sybase, and Oracle (all supported databases except DB2). + # + # == More examples + # + # Not all migrations change the schema. Some just fix the data: + # + # class RemoveEmptyTags < ActiveRecord::Migration + # def self.up + # Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? } + # end + # + # def self.down + # # not much we can do to restore deleted data + # raise IrreversibleMigration + # end + # end + # + # Others remove columns when they migrate up instead of down: + # + # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration + # def self.up + # remove_column :items, :incomplete_items_count + # remove_column :items, :completed_items_count + # end + # + # def self.down + # add_column :items, :incomplete_items_count + # add_column :items, :completed_items_count + # end + # end + # + # And sometimes you need to do something in SQL not abstracted directly by migrations: + # + # class MakeJoinUnique < ActiveRecord::Migration + # def self.up + # execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)" + # end + # + # def self.down + # execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`" + # end + # end + # + # == Using a model after changing its table + # + # Sometimes you'll want to add a column in a migration and populate it immediately after. In that case, you'll need + # to make a call to Base#reset_column_information in order to ensure that the model has the latest column data from + # after the new column was added. Example: + # + # class AddPeopleSalary < ActiveRecord::Migration + # def self.up + # add_column :people, :salary, :integer + # Person.reset_column_information + # Person.find(:all).each do |p| + # p.salary = SalaryCalculator.compute(p) + # end + # end + # end + # + # == Controlling verbosity + # + # By default, migrations will describe the actions they are taking, writing + # them to the console as they happen, along with benchmarks describing how + # long each step took. + # + # You can quiet them down by setting ActiveRecord::Migration.verbose = false. + # + # You can also insert your own messages and benchmarks by using the #say_with_time + # method: + # + # def self.up + # ... + # say_with_time "Updating salaries..." do + # Person.find(:all).each do |p| + # p.salary = SalaryCalculator.compute(p) + # end + # end + # ... + # end + # + # The phrase "Updating salaries..." would then be printed, along with the + # benchmark for the block when the block completes. + class Migration + @@verbose = true + cattr_accessor :verbose + + class << self + def up_using_benchmarks #:nodoc: + migrate(:up) + end + + def down_using_benchmarks #:nodoc: + migrate(:down) + end + + # Execute this migration in the named direction + def migrate(direction) + return unless respond_to?(direction) + + case direction + when :up then announce "migrating" + when :down then announce "reverting" + end + + result = nil + time = Benchmark.measure { result = send("real_#{direction}") } + + case direction + when :up then announce "migrated (%.4fs)" % time.real; write + when :down then announce "reverted (%.4fs)" % time.real; write + end + + result + end + + # Because the method added may do an alias_method, it can be invoked + # recursively. We use @ignore_new_methods as a guard to indicate whether + # it is safe for the call to proceed. + def singleton_method_added(sym) #:nodoc: + return if @ignore_new_methods + + begin + @ignore_new_methods = true + + case sym + when :up, :down + klass = (class << self; self; end) + klass.send(:alias_method, "real_#{sym}", sym) + klass.send(:alias_method, sym, "#{sym}_using_benchmarks") + end + ensure + @ignore_new_methods = false + end + end + + def write(text="") + puts(text) if verbose + end + + def announce(message) + text = "#{name}: #{message}" + length = [0, 75 - text.length].max + write "== %s %s" % [text, "=" * length] + end + + def say(message, subitem=false) + write "#{subitem ? " ->" : "--"} #{message}" + end + + def say_with_time(message) + say(message) + result = nil + time = Benchmark.measure { result = yield } + say "%.4fs" % time.real, :subitem + result + end + + def suppress_messages + save = verbose + self.verbose = false + yield + ensure + self.verbose = save + end + + def method_missing(method, *arguments, &block) + say_with_time "#{method}(#{arguments.map { |a| a.inspect }.join(", ")})" do + arguments[0] = Migrator.proper_table_name(arguments.first) unless arguments.empty? || method == :execute + ActiveRecord::Base.connection.send(method, *arguments, &block) + end + end + end + end + + class Migrator#:nodoc: + class << self + def migrate(migrations_path, target_version = nil) + Base.connection.initialize_schema_information + + case + when target_version.nil?, current_version < target_version + up(migrations_path, target_version) + when current_version > target_version + down(migrations_path, target_version) + when current_version == target_version + return # You're on the right version + end + end + + def up(migrations_path, target_version = nil) + self.new(:up, migrations_path, target_version).migrate + end + + def down(migrations_path, target_version = nil) + self.new(:down, migrations_path, target_version).migrate + end + + def schema_info_table_name + Base.table_name_prefix + "schema_info" + Base.table_name_suffix + end + + def current_version + (Base.connection.select_one("SELECT version FROM #{schema_info_table_name}") || {"version" => 0})["version"].to_i + end + + def proper_table_name(name) + # Use the ActiveRecord objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string + name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}" + end + + end + + def initialize(direction, migrations_path, target_version = nil) + raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations? + @direction, @migrations_path, @target_version = direction, migrations_path, target_version + Base.connection.initialize_schema_information + end + + def current_version + self.class.current_version + end + + def migrate + migration_classes.each do |(version, migration_class)| + Base.logger.info("Reached target version: #{@target_version}") and break if reached_target_version?(version) + next if irrelevant_migration?(version) + + Base.logger.info "Migrating to #{migration_class} (#{version})" + migration_class.migrate(@direction) + set_schema_version(version) + end + end + + private + def migration_classes + migrations = migration_files.inject([]) do |migrations, migration_file| + load(migration_file) + version, name = migration_version_and_name(migration_file) + assert_unique_migration_version(migrations, version.to_i) + migrations << [ version.to_i, migration_class(name) ] + end + + down? ? migrations.sort.reverse : migrations.sort + end + + def assert_unique_migration_version(migrations, version) + if !migrations.empty? && migrations.transpose.first.include?(version) + raise DuplicateMigrationVersionError.new(version) + end + end + + def migration_files + files = Dir["#{@migrations_path}/[0-9]*_*.rb"].sort_by do |f| + migration_version_and_name(f).first.to_i + end + down? ? files.reverse : files + end + + def migration_class(migration_name) + migration_name.camelize.constantize + end + + def migration_version_and_name(migration_file) + return *migration_file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first + end + + def set_schema_version(version) + Base.connection.update("UPDATE #{self.class.schema_info_table_name} SET version = #{down? ? version.to_i - 1 : version.to_i}") + end + + def up? + @direction == :up + end + + def down? + @direction == :down + end + + def reached_target_version?(version) + (up? && version.to_i - 1 == @target_version) || (down? && version.to_i == @target_version) + end + + def irrelevant_migration?(version) + (up? && version.to_i <= current_version) || (down? && version.to_i > current_version) + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/observer.rb b/vendor/rails/activerecord/lib/active_record/observer.rb new file mode 100644 index 00000000..97aa5887 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/observer.rb @@ -0,0 +1,139 @@ +require 'singleton' + +module ActiveRecord + module Observing # :nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + end + + module ClassMethods + # Activates the observers assigned. Examples: + # + # # Calls PersonObserver.instance + # ActiveRecord::Base.observers = :person_observer + # + # # Calls Cacher.instance and GarbageCollector.instance + # ActiveRecord::Base.observers = :cacher, :garbage_collector + # + # # Same as above, just using explicit class references + # ActiveRecord::Base.observers = Cacher, GarbageCollector + def observers=(*observers) + observers = [ observers ].flatten.each do |observer| + observer.is_a?(Symbol) ? + observer.to_s.camelize.constantize.instance : + observer.instance + end + end + end + end + + # Observer classes respond to lifecycle callbacks to implement trigger-like + # behavior outside the original class. This is a great way to reduce the + # clutter that normally comes when the model class is burdened with + # functionality that doesn't pertain to the core responsibility of the + # class. Example: + # + # class CommentObserver < ActiveRecord::Observer + # def after_save(comment) + # Notifications.deliver_comment("admin@do.com", "New comment was posted", comment) + # end + # end + # + # This Observer sends an email when a Comment#save is finished. + # + # class ContactObserver < ActiveRecord::Observer + # def after_create(contact) + # contact.logger.info('New contact added!') + # end + # + # def after_destroy(contact) + # contact.logger.warn("Contact with an id of #{contact.id} was destroyed!") + # end + # end + # + # This Observer uses logger to log when specific callbacks are triggered. + # + # == Observing a class that can't be inferred + # + # Observers will by default be mapped to the class with which they share a name. So CommentObserver will + # be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer + # differently than the class you're interested in observing, you can use the Observer.observe class method: + # + # class AuditObserver < ActiveRecord::Observer + # observe Account + # + # def after_update(account) + # AuditTrail.new(account, "UPDATED") + # end + # end + # + # If the audit observer needs to watch more than one kind of object, this can be specified with multiple arguments: + # + # class AuditObserver < ActiveRecord::Observer + # observe Account, Balance + # + # def after_update(record) + # AuditTrail.new(record, "UPDATED") + # end + # end + # + # The AuditObserver will now act on both updates to Account and Balance by treating them both as records. + # + # == Available callback methods + # + # The observer can implement callback methods for each of the methods described in the Callbacks module. + # + # == Storing Observers in Rails + # + # If you're using Active Record within Rails, observer classes are usually stored in app/models with the + # naming convention of app/models/audit_observer.rb. + # + # == Configuration + # + # In order to activate an observer, list it in the config.active_record.observers configuration setting in your + # config/environment.rb file. + # + # config.active_record.observers = :comment_observer, :signup_observer + # + # Observers will not be invoked unless you define these in your application configuration. + # + class Observer + include Singleton + + # Observer subclasses should be reloaded by the dispatcher in Rails + # when Dependencies.mechanism = :load. + include Reloadable::Subclasses + + # Attaches the observer to the supplied model classes. + def self.observe(*models) + define_method(:observed_class) { models } + end + + def initialize + observed_classes = [ observed_class ].flatten + observed_subclasses_class = observed_classes.collect {|c| c.send(:subclasses) }.flatten! + (observed_classes + observed_subclasses_class).each do |klass| + klass.add_observer(self) + klass.send(:define_method, :after_find) unless klass.respond_to?(:after_find) + end + end + + def update(callback_method, object) #:nodoc: + send(callback_method, object) if respond_to?(callback_method) + end + + private + def observed_class + if self.class.respond_to? "observed_class" + self.class.observed_class + else + Object.const_get(infer_observed_class_name) + end + end + + def infer_observed_class_name + self.class.name.scan(/(.*)Observer/)[0][0] + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/query_cache.rb b/vendor/rails/activerecord/lib/active_record/query_cache.rb new file mode 100644 index 00000000..e79b3e05 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/query_cache.rb @@ -0,0 +1,64 @@ +module ActiveRecord + class QueryCache #:nodoc: + def initialize(connection) + @connection = connection + @query_cache = {} + end + + def clear_query_cache + @query_cache = {} + end + + def select_all(sql, name = nil) + (@query_cache[sql] ||= @connection.select_all(sql, name)).dup + end + + def select_one(sql, name = nil) + @query_cache[sql] ||= @connection.select_one(sql, name) + end + + def columns(table_name, name = nil) + @query_cache["SHOW FIELDS FROM #{table_name}"] ||= @connection.columns(table_name, name) + end + + def insert(sql, name = nil, pk = nil, id_value = nil) + clear_query_cache + @connection.insert(sql, name, pk, id_value) + end + + def update(sql, name = nil) + clear_query_cache + @connection.update(sql, name) + end + + def delete(sql, name = nil) + clear_query_cache + @connection.delete(sql, name) + end + + private + def method_missing(method, *arguments, &proc) + @connection.send(method, *arguments, &proc) + end + end + + class Base + # Set the connection for the class with caching on + class << self + alias_method :connection_without_query_cache=, :connection= + + def connection=(spec) + if spec.is_a?(ConnectionSpecification) and spec.config[:query_cache] + spec = QueryCache.new(self.send(spec.adapter_method, spec.config)) + end + self.connection_without_query_cache = spec + end + end + end + + class AbstractAdapter #:nodoc: + # Stub method to be able to treat the connection the same whether the query cache has been turned on or not + def clear_query_cache + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/reflection.rb b/vendor/rails/activerecord/lib/active_record/reflection.rb new file mode 100644 index 00000000..aab54445 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/reflection.rb @@ -0,0 +1,204 @@ +module ActiveRecord + module Reflection # :nodoc: + def self.included(base) + base.extend(ClassMethods) + end + + # Reflection allows you to interrogate Active Record classes and objects about their associations and aggregations. + # This information can, for example, be used in a form builder that took an Active Record object and created input + # fields for all of the attributes depending on their type and displayed the associations to other objects. + # + # You can find the interface for the AggregateReflection and AssociationReflection classes in the abstract MacroReflection class. + module ClassMethods + def create_reflection(macro, name, options, active_record) + case macro + when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many + reflection = AssociationReflection.new(macro, name, options, active_record) + when :composed_of + reflection = AggregateReflection.new(macro, name, options, active_record) + end + write_inheritable_hash :reflections, name => reflection + reflection + end + + def reflections + read_inheritable_attribute(:reflections) or write_inheritable_attribute(:reflections, {}) + end + + # Returns an array of AggregateReflection objects for all the aggregations in the class. + def reflect_on_all_aggregations + reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) } + end + + # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). Example: + # Account.reflect_on_aggregation(:balance) # returns the balance AggregateReflection + def reflect_on_aggregation(aggregation) + reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil + end + + # Returns an array of AssociationReflection objects for all the aggregations in the class. If you only want to reflect on a + # certain association type, pass in the symbol (:has_many, :has_one, :belongs_to) for that as the first parameter. Example: + # Account.reflect_on_all_associations # returns an array of all associations + # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations + def reflect_on_all_associations(macro = nil) + association_reflections = reflections.values.select { |reflection| reflection.is_a?(AssociationReflection) } + macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections + end + + # Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example: + # Account.reflect_on_association(:owner) # returns the owner AssociationReflection + # Invoice.reflect_on_association(:line_items).macro # returns :has_many + def reflect_on_association(association) + reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil + end + end + + + # Abstract base class for AggregateReflection and AssociationReflection that describes the interface available for both of + # those classes. Objects of AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. + class MacroReflection + attr_reader :active_record + def initialize(macro, name, options, active_record) + @macro, @name, @options, @active_record = macro, name, options, active_record + end + + # Returns the name of the macro, so it would return :balance for "composed_of :balance, :class_name => 'Money'" or + # :clients for "has_many :clients". + def name + @name + end + + # Returns the name of the macro, so it would return :composed_of for + # "composed_of :balance, :class_name => 'Money'" or :has_many for "has_many :clients". + def macro + @macro + end + + # Returns the hash of options used for the macro, so it would return { :class_name => "Money" } for + # "composed_of :balance, :class_name => 'Money'" or {} for "has_many :clients". + def options + @options + end + + # Returns the class for the macro, so "composed_of :balance, :class_name => 'Money'" would return the Money class and + # "has_many :clients" would return the Client class. + def klass() end + + def class_name + @class_name ||= name_to_class_name(name.id2name) + end + + def ==(other_aggregation) + name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record + end + end + + + # Holds all the meta-data about an aggregation as it was specified in the Active Record class. + class AggregateReflection < MacroReflection #:nodoc: + def klass + @klass ||= Object.const_get(options[:class_name] || class_name) + end + + private + def name_to_class_name(name) + name.capitalize.gsub(/_(.)/) { |s| $1.capitalize } + end + end + + # Holds all the meta-data about an association as it was specified in the Active Record class. + class AssociationReflection < MacroReflection #:nodoc: + def klass + @klass ||= active_record.send(:compute_type, class_name) + end + + def table_name + @table_name ||= klass.table_name + end + + def primary_key_name + return @primary_key_name if @primary_key_name + case + when macro == :belongs_to + @primary_key_name = options[:foreign_key] || class_name.foreign_key + when options[:as] + @primary_key_name = options[:foreign_key] || "#{options[:as]}_id" + else + @primary_key_name = options[:foreign_key] || active_record.name.foreign_key + end + end + + def association_foreign_key + @association_foreign_key ||= @options[:association_foreign_key] || class_name.foreign_key + end + + def counter_cache_column + if options[:counter_cache] == true + "#{active_record.name.underscore.pluralize}_count" + elsif options[:counter_cache] + options[:counter_cache] + end + end + + def through_reflection + @through_reflection ||= options[:through] ? active_record.reflect_on_association(options[:through]) : false + end + + # Gets an array of possible :through source reflection names + # + # [singularized, pluralized] + def source_reflection_names + @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym } + end + + # Gets the source of the through reflection. It checks both a singularized and pluralized form for :belongs_to or :has_many. + # (The :tags association on Tagging below) + # + # class Post + # has_many :tags, :through => :taggings + # end + # + def source_reflection + return nil unless through_reflection + @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first + end + + def check_validity! + if options[:through] + if through_reflection.nil? + raise HasManyThroughAssociationNotFoundError.new(self) + end + + if source_reflection.nil? + raise HasManyThroughSourceAssociationNotFoundError.new(self) + end + + if source_reflection.options[:polymorphic] + raise HasManyThroughAssociationPolymorphicError.new(class_name, self, source_reflection) + end + + unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil? + raise HasManyThroughSourceAssociationMacroError.new(self) + end + end + end + + private + def name_to_class_name(name) + if name =~ /::/ + name + else + if options[:class_name] + options[:class_name] + elsif through_reflection # get the class_name of the belongs_to association of the through reflection + source_reflection.class_name + else + class_name = name.to_s.camelize + class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro) + class_name + end + end + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/schema.rb b/vendor/rails/activerecord/lib/active_record/schema.rb new file mode 100644 index 00000000..dc854463 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/schema.rb @@ -0,0 +1,58 @@ +module ActiveRecord + # Allows programmers to programmatically define a schema in a portable + # DSL. This means you can define tables, indexes, etc. without using SQL + # directly, so your applications can more easily support multiple + # databases. + # + # Usage: + # + # ActiveRecord::Schema.define do + # create_table :authors do |t| + # t.column :name, :string, :null => false + # end + # + # add_index :authors, :name, :unique + # + # create_table :posts do |t| + # t.column :author_id, :integer, :null => false + # t.column :subject, :string + # t.column :body, :text + # t.column :private, :boolean, :default => false + # end + # + # add_index :posts, :author_id + # end + # + # ActiveRecord::Schema is only supported by database adapters that also + # support migrations, the two features being very similar. + class Schema < Migration + private_class_method :new + + # Eval the given block. All methods available to the current connection + # adapter are available within the block, so you can easily use the + # database definition DSL to build up your schema (#create_table, + # #add_index, etc.). + # + # The +info+ hash is optional, and if given is used to define metadata + # about the current schema (like the schema's version): + # + # ActiveRecord::Schema.define(:version => 15) do + # ... + # end + def self.define(info={}, &block) + instance_eval(&block) + + unless info.empty? + initialize_schema_information + cols = columns('schema_info') + + info = info.map do |k,v| + v = Base.connection.quote(v, cols.detect { |c| c.name == k.to_s }) + "#{k} = #{v}" + end + + Base.connection.update "UPDATE #{Migrator.schema_info_table_name} SET #{info.join(", ")}" + end + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/schema_dumper.rb b/vendor/rails/activerecord/lib/active_record/schema_dumper.rb new file mode 100644 index 00000000..06fdd3f8 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/schema_dumper.rb @@ -0,0 +1,121 @@ +module ActiveRecord + # This class is used to dump the database schema for some connection to some + # output format (i.e., ActiveRecord::Schema). + class SchemaDumper #:nodoc: + private_class_method :new + + # A list of tables which should not be dumped to the schema. + # Acceptable values are strings as well as regexp. + # This setting is only used if ActiveRecord::Base.schema_format == :ruby + cattr_accessor :ignore_tables + @@ignore_tables = [] + + def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT) + new(connection).dump(stream) + stream + end + + def dump(stream) + header(stream) + tables(stream) + trailer(stream) + stream + end + + private + + def initialize(connection) + @connection = connection + @types = @connection.native_database_types + @info = @connection.select_one("SELECT * FROM schema_info") rescue nil + end + + def header(stream) + define_params = @info ? ":version => #{@info['version']}" : "" + + stream.puts <
      "#{pk}") + end + else + tbl.print ", :id => false" + end + tbl.print ", :force => true" + tbl.puts " do |t|" + + columns.each do |column| + raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil? + next if column.name == pk + tbl.print " t.column #{column.name.inspect}, #{column.type.inspect}" + tbl.print ", :limit => #{column.limit.inspect}" if column.limit != @types[column.type][:limit] + tbl.print ", :default => #{column.default.inspect}" if !column.default.nil? + tbl.print ", :null => false" if !column.null + tbl.puts + end + + tbl.puts " end" + tbl.puts + + indexes(table, tbl) + + tbl.rewind + stream.print tbl.read + rescue => e + stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}" + stream.puts "# #{e.message}" + stream.puts + end + + stream + end + + def indexes(table, stream) + indexes = @connection.indexes(table) + indexes.each do |index| + stream.print " add_index #{index.table.inspect}, #{index.columns.inspect}, :name => #{index.name.inspect}" + stream.print ", :unique => true" if index.unique + stream.puts + end + stream.puts unless indexes.empty? + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/timestamp.rb b/vendor/rails/activerecord/lib/active_record/timestamp.rb new file mode 100644 index 00000000..3c947f0e --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/timestamp.rb @@ -0,0 +1,62 @@ +module ActiveRecord + # Active Records will automatically record creation and/or update timestamps of database objects + # if fields of the names created_at/created_on or updated_at/updated_on are present. This module is + # automatically included, so you don't need to do that manually. + # + # This behavior can be turned off by setting ActiveRecord::Base.record_timestamps = false. + # This behavior by default uses local time, but can use UTC by setting ActiveRecord::Base.default_timezone = :utc + module Timestamp + def self.append_features(base) # :nodoc: + super + + base.class_eval do + alias_method :create_without_timestamps, :create + alias_method :create, :create_with_timestamps + + alias_method :update_without_timestamps, :update + alias_method :update, :update_with_timestamps + end + end + + def create_with_timestamps #:nodoc: + if record_timestamps + t = ( self.class.default_timezone == :utc ? Time.now.utc : Time.now ) + write_attribute('created_at', t) if respond_to?(:created_at) && created_at.nil? + write_attribute('created_on', t) if respond_to?(:created_on) && created_on.nil? + + write_attribute('updated_at', t) if respond_to?(:updated_at) + write_attribute('updated_on', t) if respond_to?(:updated_on) + end + create_without_timestamps + end + + def update_with_timestamps #:nodoc: + if record_timestamps + t = ( self.class.default_timezone == :utc ? Time.now.utc : Time.now ) + write_attribute('updated_at', t) if respond_to?(:updated_at) + write_attribute('updated_on', t) if respond_to?(:updated_on) + end + update_without_timestamps + end + end + + class Base + # Records the creation date and possibly time in created_on (date only) or created_at (date and time) and the update date and possibly + # time in updated_on and updated_at. This only happens if the object responds to either of these messages, which they will do automatically + # if the table has columns of either of these names. This feature is turned on by default. + @@record_timestamps = true + cattr_accessor :record_timestamps + + # deprecated: use ActiveRecord::Base.default_timezone instead. + @@timestamps_gmt = false + def self.timestamps_gmt=( gmt ) #:nodoc: + warn "timestamps_gmt= is deprecated. use default_timezone= instead" + self.default_timezone = ( gmt ? :utc : :local ) + end + + def self.timestamps_gmt #:nodoc: + warn "timestamps_gmt is deprecated. use default_timezone instead" + self.default_timezone == :utc + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/transactions.rb b/vendor/rails/activerecord/lib/active_record/transactions.rb new file mode 100644 index 00000000..2c5692c0 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/transactions.rb @@ -0,0 +1,129 @@ +require 'active_record/vendor/simple.rb' +Transaction::Simple.send(:remove_method, :transaction) +require 'thread' + +module ActiveRecord + module Transactions # :nodoc: + TRANSACTION_MUTEX = Mutex.new + + class TransactionError < ActiveRecordError # :nodoc: + end + + def self.append_features(base) + super + base.extend(ClassMethods) + + base.class_eval do + alias_method :destroy_without_transactions, :destroy + alias_method :destroy, :destroy_with_transactions + + alias_method :save_without_transactions, :save + alias_method :save, :save_with_transactions + end + end + + # Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action. + # The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succeeded and + # vice versa. Transactions enforce the integrity of the database and guard the data against program errors or database break-downs. + # So basically you should use transaction blocks whenever you have a number of statements that must be executed together or + # not at all. Example: + # + # transaction do + # david.withdrawal(100) + # mary.deposit(100) + # end + # + # This example will only take money from David and give to Mary if neither +withdrawal+ nor +deposit+ raises an exception. + # Exceptions will force a ROLLBACK that returns the database to the state before the transaction was begun. Be aware, though, + # that the objects by default will _not_ have their instance data returned to their pre-transactional state. + # + # == Transactions are not distributed across database connections + # + # A transaction acts on a single database connection. If you have + # multiple class-specific databases, the transaction will not protect + # interaction among them. One workaround is to begin a transaction + # on each class whose models you alter: + # + # Student.transaction do + # Course.transaction do + # course.enroll(student) + # student.units += course.units + # end + # end + # + # This is a poor solution, but full distributed transactions are beyond + # the scope of Active Record. + # + # == Save and destroy are automatically wrapped in a transaction + # + # Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks + # will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction + # depend on or you can raise exceptions in the callbacks to rollback. + # + # == Object-level transactions + # + # You can enable object-level transactions for Active Record objects, though. You do this by naming each of the Active Records + # that you want to enable object-level transactions for, like this: + # + # Account.transaction(david, mary) do + # david.withdrawal(100) + # mary.deposit(100) + # end + # + # If the transaction fails, David and Mary will be returned to their pre-transactional state. No money will have changed hands in + # neither object nor database. + # + # == Exception handling + # + # Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you + # should be ready to catch those in your application code. + # + # Tribute: Object-level transactions are implemented by Transaction::Simple by Austin Ziegler. + module ClassMethods + def transaction(*objects, &block) + previous_handler = trap('TERM') { raise TransactionError, "Transaction aborted" } + lock_mutex + + begin + objects.each { |o| o.extend(Transaction::Simple) } + objects.each { |o| o.start_transaction } + + result = connection.transaction(Thread.current['start_db_transaction'], &block) + + objects.each { |o| o.commit_transaction } + return result + rescue Exception => object_transaction_rollback + objects.each { |o| o.abort_transaction } + raise + ensure + unlock_mutex + trap('TERM', previous_handler) + end + end + + def lock_mutex#:nodoc: + Thread.current['open_transactions'] ||= 0 + TRANSACTION_MUTEX.lock if Thread.current['open_transactions'] == 0 + Thread.current['start_db_transaction'] = (Thread.current['open_transactions'] == 0) + Thread.current['open_transactions'] += 1 + end + + def unlock_mutex#:nodoc: + Thread.current['open_transactions'] -= 1 + TRANSACTION_MUTEX.unlock if Thread.current['open_transactions'] == 0 + end + end + + def transaction(*objects, &block) + self.class.transaction(*objects, &block) + end + + def destroy_with_transactions #:nodoc: + transaction { destroy_without_transactions } + end + + def save_with_transactions(perform_validation = true) #:nodoc: + transaction { save_without_transactions(perform_validation) } + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/validations.rb b/vendor/rails/activerecord/lib/active_record/validations.rb new file mode 100755 index 00000000..f34871c3 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/validations.rb @@ -0,0 +1,827 @@ +module ActiveRecord + # Raised by save! and create! when the record is invalid. Use the + # record method to retrieve the record which did not validate. + # begin + # complex_operation_that_calls_save!_internally + # rescue ActiveRecord::RecordInvalid => invalid + # puts invalid.record.errors + # end + class RecordInvalid < ActiveRecordError #:nodoc: + attr_reader :record + def initialize(record) + @record = record + super("Validation failed: #{@record.errors.full_messages.join(", ")}") + end + end + + # Active Record validation is reported to and from this object, which is used by Base#save to + # determine whether the object in a valid state to be saved. See usage example in Validations. + class Errors + include Enumerable + + def initialize(base) # :nodoc: + @base, @errors = base, {} + end + + @@default_error_messages = { + :inclusion => "is not included in the list", + :exclusion => "is reserved", + :invalid => "is invalid", + :confirmation => "doesn't match confirmation", + :accepted => "must be accepted", + :empty => "can't be empty", + :blank => "can't be blank", + :too_long => "is too long (maximum is %d characters)", + :too_short => "is too short (minimum is %d characters)", + :wrong_length => "is the wrong length (should be %d characters)", + :taken => "has already been taken", + :not_a_number => "is not a number" + } + + # Holds a hash with all the default error messages, such that they can be replaced by your own copy or localizations. + cattr_accessor :default_error_messages + + + # Adds an error to the base object instead of any particular attribute. This is used + # to report errors that don't tie to any specific attribute, but rather to the object + # as a whole. These error messages don't get prepended with any field name when iterating + # with each_full, so they should be complete sentences. + def add_to_base(msg) + add(:base, msg) + end + + # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to on(attribute) + # for the same attribute and ensure that this error object returns false when asked if empty?. More than one + # error can be added to the same +attribute+ in which case an array will be returned on a call to on(attribute). + # If no +msg+ is supplied, "invalid" is assumed. + def add(attribute, msg = @@default_error_messages[:invalid]) + @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil? + @errors[attribute.to_s] << msg + end + + # Will add an error message to each of the attributes in +attributes+ that is empty. + def add_on_empty(attributes, msg = @@default_error_messages[:empty]) + for attr in [attributes].flatten + value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] + is_empty = value.respond_to?("empty?") ? value.empty? : false + add(attr, msg) unless !value.nil? && !is_empty + end + end + + # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). + def add_on_blank(attributes, msg = @@default_error_messages[:blank]) + for attr in [attributes].flatten + value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] + add(attr, msg) if value.blank? + end + end + + # Will add an error message to each of the attributes in +attributes+ that has a length outside of the passed boundary +range+. + # If the length is above the boundary, the too_long_msg message will be used. If below, the too_short_msg. + def add_on_boundary_breaking(attributes, range, too_long_msg = @@default_error_messages[:too_long], too_short_msg = @@default_error_messages[:too_short]) + for attr in [attributes].flatten + value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] + add(attr, too_short_msg % range.begin) if value && value.length < range.begin + add(attr, too_long_msg % range.end) if value && value.length > range.end + end + end + + alias :add_on_boundry_breaking :add_on_boundary_breaking + + # Returns true if the specified +attribute+ has errors associated with it. + def invalid?(attribute) + !@errors[attribute.to_s].nil? + end + + # * Returns nil, if no errors are associated with the specified +attribute+. + # * Returns the error message, if one error is associated with the specified +attribute+. + # * Returns an array of error messages, if more than one error is associated with the specified +attribute+. + def on(attribute) + if @errors[attribute.to_s].nil? + nil + elsif @errors[attribute.to_s].length == 1 + @errors[attribute.to_s].first + else + @errors[attribute.to_s] + end + end + + alias :[] :on + + # Returns errors assigned to base object through add_to_base according to the normal rules of on(attribute). + def on_base + on(:base) + end + + # Yields each attribute and associated message per error added. + def each + @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } } + end + + # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned + # through iteration as "First name can't be empty". + def each_full + full_messages.each { |msg| yield msg } + end + + # Returns all the full error messages in an array. + def full_messages + full_messages = [] + + @errors.each_key do |attr| + @errors[attr].each do |msg| + next if msg.nil? + + if attr == "base" + full_messages << msg + else + full_messages << @base.class.human_attribute_name(attr) + " " + msg + end + end + end + + return full_messages + end + + # Returns true if no errors have been added. + def empty? + return @errors.empty? + end + + # Removes all the errors that have been added. + def clear + @errors = {} + end + + # Returns the total number of errors added. Two errors added to the same attribute will be counted as such + # with this as well. + def size + error_count = 0 + @errors.each_value { |attribute| error_count += attribute.length } + error_count + end + + alias_method :count, :size + alias_method :length, :size + end + + + # Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and + # +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring + # that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression). + # + # Example: + # + # class Person < ActiveRecord::Base + # protected + # def validate + # errors.add_on_empty %w( first_name last_name ) + # errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/ + # end + # + # def validate_on_create # is only run the first time a new object is saved + # unless valid_discount?(membership_discount) + # errors.add("membership_discount", "has expired") + # end + # end + # + # def validate_on_update + # errors.add_to_base("No changes have occurred") if unchanged_attributes? + # end + # end + # + # person = Person.new("first_name" => "David", "phone_number" => "what?") + # person.save # => false (and doesn't do the save) + # person.errors.empty? # => false + # person.errors.count # => 2 + # person.errors.on "last_name" # => "can't be empty" + # person.errors.on "phone_number" # => "has invalid format" + # person.errors.each_full { |msg| puts msg } + # # => "Last name can't be empty\n" + + # "Phone number has invalid format" + # + # person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" } + # person.save # => true (and person is now saved in the database) + # + # An +Errors+ object is automatically created for every Active Record. + # + # Please do have a look at ActiveRecord::Validations::ClassMethods for a higher level of validations. + module Validations + VALIDATIONS = %w( validate validate_on_create validate_on_update ) + + def self.append_features(base) # :nodoc: + super + base.extend ClassMethods + base.class_eval do + alias_method :save_without_validation, :save + alias_method :save, :save_with_validation + + alias_method :save_without_validation!, :save! + alias_method :save!, :save_with_validation! + + alias_method :update_attribute_without_validation_skipping, :update_attribute + alias_method :update_attribute, :update_attribute_with_validation_skipping + end + end + + # All of the following validations are defined in the class scope of the model that you're interested in validating. + # They offer a more declarative way of specifying when the model is valid and when it is not. It is recommended to use + # these over the low-level calls to validate and validate_on_create when possible. + module ClassMethods + DEFAULT_VALIDATION_OPTIONS = { + :on => :save, + :allow_nil => false, + :message => nil + }.freeze + + ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze + + def validate(*methods, &block) + methods << block if block_given? + write_inheritable_set(:validate, methods) + end + + def validate_on_create(*methods, &block) + methods << block if block_given? + write_inheritable_set(:validate_on_create, methods) + end + + def validate_on_update(*methods, &block) + methods << block if block_given? + write_inheritable_set(:validate_on_update, methods) + end + + def condition_block?(condition) + condition.respond_to?("call") && (condition.arity == 1 || condition.arity == -1) + end + + # Determine from the given condition (whether a block, procedure, method or string) + # whether or not to validate the record. See #validates_each. + def evaluate_condition(condition, record) + case condition + when Symbol: record.send(condition) + when String: eval(condition, binding) + else + if condition_block?(condition) + condition.call(record) + else + raise( + ActiveRecordError, + "Validations need to be either a symbol, string (to be eval'ed), proc/method, or " + + "class implementing a static validation method" + ) + end + end + end + + # Validates each attribute against a block. + # + # class Person < ActiveRecord::Base + # validates_each :first_name, :last_name do |record, attr, value| + # record.errors.add attr, 'starts with z.' if value[0] == ?z + # end + # end + # + # Options: + # * on - Specifies when this validation is active (default is :save, other options :create, :update) + # * allow_nil - Skip validation if attribute is nil. + # * if - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The + # method, proc or string should return or evaluate to a true or false value. + def validates_each(*attrs) + options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {} + attrs = attrs.flatten + + # Declare the validation. + send(validation_method(options[:on] || :save)) do |record| + # Don't validate when there is an :if condition and that condition is false + unless options[:if] && !evaluate_condition(options[:if], record) + attrs.each do |attr| + value = record.send(attr) + next if value.nil? && options[:allow_nil] + yield record, attr, value + end + end + end + end + + # Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example: + # + # Model: + # class Person < ActiveRecord::Base + # validates_confirmation_of :user_name, :password + # validates_confirmation_of :email_address, :message => "should match confirmation" + # end + # + # View: + # <%= password_field "person", "password" %> + # <%= password_field "person", "password_confirmation" %> + # + # The person has to already have a password attribute (a column in the people table), but the password_confirmation is virtual. + # It exists only as an in-memory variable for validating the password. This check is performed only if password_confirmation + # is not nil and by default on save. + # + # Configuration options: + # * message - A custom error message (default is: "doesn't match confirmation") + # * on - Specifies when this validation is active (default is :save, other options :create, :update) + # * if - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The + # method, proc or string should return or evaluate to a true or false value. + def validates_confirmation_of(*attr_names) + configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save } + configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) + + attr_accessor *(attr_names.map { |n| "#{n}_confirmation" }) + + validates_each(attr_names, configuration) do |record, attr_name, value| + record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") + end + end + + # Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example: + # + # class Person < ActiveRecord::Base + # validates_acceptance_of :terms_of_service + # validates_acceptance_of :eula, :message => "must be abided" + # end + # + # The terms_of_service attribute is entirely virtual. No database column is needed. This check is performed only if + # terms_of_service is not nil and by default on save. + # + # Configuration options: + # * message - A custom error message (default is: "must be accepted") + # * on - Specifies when this validation is active (default is :save, other options :create, :update) + # * accept - Specifies value that is considered accepted. The default value is a string "1", which + # makes it easy to relate to an HTML checkbox. + # * if - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The + # method, proc or string should return or evaluate to a true or false value. + def validates_acceptance_of(*attr_names) + configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" } + configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) + + attr_accessor *attr_names + + validates_each(attr_names,configuration) do |record, attr_name, value| + record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept] + end + end + + # Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save. Example: + # + # class Person < ActiveRecord::Base + # validates_presence_of :first_name + # end + # + # The first_name attribute must be in the object and it cannot be blank. + # + # Configuration options: + # * message - A custom error message (default is: "can't be blank") + # * on - Specifies when this validation is active (default is :save, other options :create, :update) + # * if - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The + # method, proc or string should return or evaluate to a true or false value. + # + # === Warning + # Validate the presence of the foreign key, not the instance variable itself. + # Do this: + # validate_presence_of :invoice_id + # + # Not this: + # validate_presence_of :invoice + # + # If you validate the presence of the associated object, you will get + # failures on saves when both the parent object and the child object are + # new. + def validates_presence_of(*attr_names) + configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save } + configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) + + # can't use validates_each here, because it cannot cope with nonexistent attributes, + # while errors.add_on_empty can + attr_names.each do |attr_name| + send(validation_method(configuration[:on])) do |record| + unless configuration[:if] and not evaluate_condition(configuration[:if], record) + record.errors.add_on_blank(attr_name,configuration[:message]) + end + end + end + end + + # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: + # + # class Person < ActiveRecord::Base + # validates_length_of :first_name, :maximum=>30 + # validates_length_of :last_name, :maximum=>30, :message=>"less than %d if you don't mind" + # validates_length_of :fax, :in => 7..32, :allow_nil => true + # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name" + # validates_length_of :fav_bra_size, :minimum=>1, :too_short=>"please enter at least %d character" + # validates_length_of :smurf_leader, :is=>4, :message=>"papa is spelled with %d characters... don't play me." + # end + # + # Configuration options: + # * minimum - The minimum size of the attribute + # * maximum - The maximum size of the attribute + # * is - The exact size of the attribute + # * within - A range specifying the minimum and maximum size of the attribute + # * in - A synonym(or alias) for :within + # * allow_nil - Attribute may be nil; skip validation. + # + # * too_long - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)") + # * too_short - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)") + # * wrong_length - The error message if using the :is method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)") + # * message - The error message to use for a :minimum, :maximum, or :is violation. An alias of the appropriate too_long/too_short/wrong_length message + # * on - Specifies when this validation is active (default is :save, other options :create, :update) + # * if - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The + # method, proc or string should return or evaluate to a true or false value. + def validates_length_of(*attrs) + # Merge given options with defaults. + options = { + :too_long => ActiveRecord::Errors.default_error_messages[:too_long], + :too_short => ActiveRecord::Errors.default_error_messages[:too_short], + :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length] + }.merge(DEFAULT_VALIDATION_OPTIONS) + options.update(attrs.pop.symbolize_keys) if attrs.last.is_a?(Hash) + + # Ensure that one and only one range option is specified. + range_options = ALL_RANGE_OPTIONS & options.keys + case range_options.size + when 0 + raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' + when 1 + # Valid number of options; do nothing. + else + raise ArgumentError, 'Too many range options specified. Choose only one.' + end + + # Get range option and value. + option = range_options.first + option_value = options[range_options.first] + + case option + when :within, :in + raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) + + too_short = options[:too_short] % option_value.begin + too_long = options[:too_long] % option_value.end + + validates_each(attrs, options) do |record, attr, value| + if value.nil? or value.split(//).size < option_value.begin + record.errors.add(attr, too_short) + elsif value.split(//).size > option_value.end + record.errors.add(attr, too_long) + end + end + when :is, :minimum, :maximum + raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0 + + # Declare different validations per option. + validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } + message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long } + + message = (options[:message] || options[message_options[option]]) % option_value + + validates_each(attrs, options) do |record, attr, value| + if value.kind_of?(String) + record.errors.add(attr, message) unless !value.nil? and value.split(//).size.method(validity_checks[option])[option_value] + else + record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value] + end + end + end + end + + alias_method :validates_size_of, :validates_length_of + + + # Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user + # can be named "davidhh". + # + # class Person < ActiveRecord::Base + # validates_uniqueness_of :user_name, :scope => :account_id + # end + # + # It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example, + # making sure that a teacher can only be on the schedule once per semester for a particular class. + # + # class TeacherSchedule < ActiveRecord::Base + # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id] + # end + # + # When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified + # attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself. + # + # Configuration options: + # * message - Specifies a custom error message (default is: "has already been taken") + # * scope - One or more columns by which to limit the scope of the uniquness constraint. + # * if - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The + # method, proc or string should return or evaluate to a true or false value. + + def validates_uniqueness_of(*attr_names) + configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken] } + configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) + + validates_each(attr_names,configuration) do |record, attr_name, value| + condition_sql = "#{record.class.table_name}.#{attr_name} #{attribute_condition(value)}" + condition_params = [value] + if scope = configuration[:scope] + Array(scope).map do |scope_item| + scope_value = record.send(scope_item) + condition_sql << " AND #{record.class.table_name}.#{scope_item} #{attribute_condition(scope_value)}" + condition_params << scope_value + end + end + unless record.new_record? + condition_sql << " AND #{record.class.table_name}.#{record.class.primary_key} <> ?" + condition_params << record.send(:id) + end + if record.class.find(:first, :conditions => [condition_sql, *condition_params]) + record.errors.add(attr_name, configuration[:message]) + end + end + end + + # Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression + # provided. + # + # class Person < ActiveRecord::Base + # validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :on => :create + # end + # + # A regular expression must be provided or else an exception will be raised. + # + # Configuration options: + # * message - A custom error message (default is: "is invalid") + # * with - The regular expression used to validate the format with (note: must be supplied!) + # * on Specifies when this validation is active (default is :save, other options :create, :update) + # * if - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The + # method, proc or string should return or evaluate to a true or false value. + def validates_format_of(*attr_names) + configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil } + configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) + + raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp) + + validates_each(attr_names, configuration) do |record, attr_name, value| + record.errors.add(attr_name, configuration[:message]) unless value.to_s =~ configuration[:with] + end + end + + # Validates whether the value of the specified attribute is available in a particular enumerable object. + # + # class Person < ActiveRecord::Base + # validates_inclusion_of :gender, :in=>%w( m f ), :message=>"woah! what are you then!??!!" + # validates_inclusion_of :age, :in=>0..99 + # end + # + # Configuration options: + # * in - An enumerable object of available items + # * message - Specifies a customer error message (default is: "is not included in the list") + # * allow_nil - If set to true, skips this validation if the attribute is null (default is: false) + # * if - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The + # method, proc or string should return or evaluate to a true or false value. + def validates_inclusion_of(*attr_names) + configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save } + configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) + + enum = configuration[:in] || configuration[:within] + + raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") + + validates_each(attr_names, configuration) do |record, attr_name, value| + record.errors.add(attr_name, configuration[:message]) unless enum.include?(value) + end + end + + # Validates that the value of the specified attribute is not in a particular enumerable object. + # + # class Person < ActiveRecord::Base + # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here" + # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60" + # end + # + # Configuration options: + # * in - An enumerable object of items that the value shouldn't be part of + # * message - Specifies a customer error message (default is: "is reserved") + # * allow_nil - If set to true, skips this validation if the attribute is null (default is: false) + # * if - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The + # method, proc or string should return or evaluate to a true or false value. + def validates_exclusion_of(*attr_names) + configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save } + configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) + + enum = configuration[:in] || configuration[:within] + + raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") + + validates_each(attr_names, configuration) do |record, attr_name, value| + record.errors.add(attr_name, configuration[:message]) if enum.include?(value) + end + end + + # Validates whether the associated object or objects are all valid themselves. Works with any kind of association. + # + # class Book < ActiveRecord::Base + # has_many :pages + # belongs_to :library + # + # validates_associated :pages, :library + # end + # + # Warning: If, after the above definition, you then wrote: + # + # class Page < ActiveRecord::Base + # belongs_to :book + # + # validates_associated :book + # end + # + # ...this would specify a circular dependency and cause infinite recursion. + # + # NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association + # is both present and guaranteed to be valid, you also need to use validates_presence_of. + # + # Configuration options: + # * on Specifies when this validation is active (default is :save, other options :create, :update) + # * if - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The + # method, proc or string should return or evaluate to a true or false value. + def validates_associated(*attr_names) + configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save } + configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) + + validates_each(attr_names, configuration) do |record, attr_name, value| + record.errors.add(attr_name, configuration[:message]) unless + (value.is_a?(Array) ? value : [value]).all? { |r| r.nil? or r.valid? } + end + end + + # Validates whether the value of the specified attribute is numeric by trying to convert it to + # a float with Kernel.Float (if integer is false) or applying it to the regular expression + # /^[\+\-]?\d+$/ (if integer is set to true). + # + # class Person < ActiveRecord::Base + # validates_numericality_of :value, :on => :create + # end + # + # Configuration options: + # * message - A custom error message (default is: "is not a number") + # * on Specifies when this validation is active (default is :save, other options :create, :update) + # * only_integer Specifies whether the value has to be an integer, e.g. an integral value (default is false) + # * allow_nil Skip validation if attribute is nil (default is false). Notice that for fixnum and float columns empty strings are converted to nil + # * if - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The + # method, proc or string should return or evaluate to a true or false value. + def validates_numericality_of(*attr_names) + configuration = { :message => ActiveRecord::Errors.default_error_messages[:not_a_number], :on => :save, + :only_integer => false, :allow_nil => false } + configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) + + if configuration[:only_integer] + validates_each(attr_names,configuration) do |record, attr_name,value| + record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_before_type_cast").to_s =~ /^[+-]?\d+$/ + end + else + validates_each(attr_names,configuration) do |record, attr_name,value| + next if configuration[:allow_nil] and record.send("#{attr_name}_before_type_cast").nil? + begin + Kernel.Float(record.send("#{attr_name}_before_type_cast").to_s) + rescue ArgumentError, TypeError + record.errors.add(attr_name, configuration[:message]) + end + end + end + end + + + # Creates an object just like Base.create but calls save! instead of save + # so an exception is raised if the record is invalid. + def create!(attributes = nil) + if attributes.is_a?(Array) + attributes.collect { |attr| create!(attr) } + else + attributes.reverse_merge!(scope(:create)) if scoped?(:create) + + object = new(attributes) + object.save! + object + end + end + + + private + def write_inheritable_set(key, methods) + existing_methods = read_inheritable_attribute(key) || [] + write_inheritable_attribute(key, methods | existing_methods) + end + + def validation_method(on) + case on + when :save then :validate + when :create then :validate_on_create + when :update then :validate_on_update + end + end + end + + # The validation process on save can be skipped by passing false. The regular Base#save method is + # replaced with this when the validations module is mixed in, which it is by default. + def save_with_validation(perform_validation = true) + if perform_validation && valid? || !perform_validation + save_without_validation + else + false + end + end + + # Attempts to save the record just like Base#save but will raise a RecordInvalid exception instead of returning false + # if the record is not valid. + def save_with_validation! + if valid? + save_without_validation! + else + raise RecordInvalid.new(self) + end + end + + # Updates a single attribute and saves the record without going through the normal validation procedure. + # This is especially useful for boolean flags on existing records. The regular +update_attribute+ method + # in Base is replaced with this when the validations module is mixed in, which it is by default. + def update_attribute_with_validation_skipping(name, value) + send(name.to_s + '=', value) + save(false) + end + + # Runs validate and validate_on_create or validate_on_update and returns true if no errors were added otherwise false. + def valid? + errors.clear + + run_validations(:validate) + validate + + if new_record? + run_validations(:validate_on_create) + validate_on_create + else + run_validations(:validate_on_update) + validate_on_update + end + + errors.empty? + end + + # Returns the Errors object that holds all information about attribute error messages. + def errors + @errors ||= Errors.new(self) + end + + protected + # Overwrite this method for validation checks on all saves and use Errors.add(field, msg) for invalid attributes. + def validate #:doc: + end + + # Overwrite this method for validation checks used only on creation. + def validate_on_create #:doc: + end + + # Overwrite this method for validation checks used only on updates. + def validate_on_update # :doc: + end + + private + def run_validations(validation_method) + validations = self.class.read_inheritable_attribute(validation_method.to_sym) + if validations.nil? then return end + validations.each do |validation| + if validation.is_a?(Symbol) + self.send(validation) + elsif validation.is_a?(String) + eval(validation, binding) + elsif validation_block?(validation) + validation.call(self) + elsif validation_class?(validation, validation_method) + validation.send(validation_method, self) + else + raise( + ActiveRecordError, + "Validations need to be either a symbol, string (to be eval'ed), proc/method, or " + + "class implementing a static validation method" + ) + end + end + end + + def validation_block?(validation) + validation.respond_to?("call") && (validation.arity == 1 || validation.arity == -1) + end + + def validation_class?(validation, validation_method) + validation.respond_to?(validation_method) + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/vendor/db2.rb b/vendor/rails/activerecord/lib/active_record/vendor/db2.rb new file mode 100644 index 00000000..812c8cc5 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/vendor/db2.rb @@ -0,0 +1,362 @@ +require 'db2/db2cli.rb' + +module DB2 + module DB2Util + include DB2CLI + + def free() SQLFreeHandle(@handle_type, @handle); end + def handle() @handle; end + + def check_rc(rc) + if ![SQL_SUCCESS, SQL_SUCCESS_WITH_INFO, SQL_NO_DATA_FOUND].include?(rc) + rec = 1 + msg = '' + loop do + a = SQLGetDiagRec(@handle_type, @handle, rec, 500) + break if a[0] != SQL_SUCCESS + msg << a[3] if !a[3].nil? and a[3] != '' # Create message. + rec += 1 + end + raise "DB2 error: #{msg}" + end + end + end + + class Environment + include DB2Util + + def initialize + @handle_type = SQL_HANDLE_ENV + rc, @handle = SQLAllocHandle(@handle_type, SQL_NULL_HANDLE) + check_rc(rc) + end + + def data_sources(buffer_length = 1024) + retval = [] + max_buffer_length = buffer_length + + a = SQLDataSources(@handle, SQL_FETCH_FIRST, SQL_MAX_DSN_LENGTH + 1, buffer_length) + retval << [a[1], a[3]] + max_buffer_length = [max_buffer_length, a[4]].max + + loop do + a = SQLDataSources(@handle, SQL_FETCH_NEXT, SQL_MAX_DSN_LENGTH + 1, buffer_length) + break if a[0] == SQL_NO_DATA_FOUND + + retval << [a[1], a[3]] + max_buffer_length = [max_buffer_length, a[4]].max + end + + if max_buffer_length > buffer_length + get_data_sources(max_buffer_length) + else + retval + end + end + end + + class Connection + include DB2Util + + def initialize(environment) + @env = environment + @handle_type = SQL_HANDLE_DBC + rc, @handle = SQLAllocHandle(@handle_type, @env.handle) + check_rc(rc) + end + + def connect(server_name, user_name = '', auth = '') + check_rc(SQLConnect(@handle, server_name, user_name.to_s, auth.to_s)) + end + + def set_connect_attr(attr, value) + value += "\0" if value.class == String + check_rc(SQLSetConnectAttr(@handle, attr, value)) + end + + def set_auto_commit_on + set_connect_attr(SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_ON) + end + + def set_auto_commit_off + set_connect_attr(SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF) + end + + def disconnect + check_rc(SQLDisconnect(@handle)) + end + + def rollback + check_rc(SQLEndTran(@handle_type, @handle, SQL_ROLLBACK)) + end + + def commit + check_rc(SQLEndTran(@handle_type, @handle, SQL_COMMIT)) + end + end + + class Statement + include DB2Util + + def initialize(connection) + @conn = connection + @handle_type = SQL_HANDLE_STMT + @parms = [] #yun + @sql = '' #yun + @numParms = 0 #yun + @prepared = false #yun + @parmArray = [] #yun. attributes of the parameter markers + rc, @handle = SQLAllocHandle(@handle_type, @conn.handle) + check_rc(rc) + end + + def columns(table_name, schema_name = '%') + check_rc(SQLColumns(@handle, '', schema_name.upcase, table_name.upcase, '%')) + fetch_all + end + + def tables(schema_name = '%') + check_rc(SQLTables(@handle, '', schema_name.upcase, '%', 'TABLE')) + fetch_all + end + + def indexes(table_name, schema_name = '') + check_rc(SQLStatistics(@handle, '', schema_name.upcase, table_name.upcase, SQL_INDEX_ALL, SQL_ENSURE)) + fetch_all + end + + def prepare(sql) + @sql = sql + check_rc(SQLPrepare(@handle, sql)) + rc, @numParms = SQLNumParams(@handle) #number of question marks + check_rc(rc) + #-------------------------------------------------------------------------- + # parameter attributes are stored in instance variable @parmArray so that + # they are available when execute method is called. + #-------------------------------------------------------------------------- + if @numParms > 0 # get parameter marker attributes + 1.upto(@numParms) do |i| # parameter number starts from 1 + rc, type, size, decimalDigits = SQLDescribeParam(@handle, i) + check_rc(rc) + @parmArray << Parameter.new(type, size, decimalDigits) + end + end + @prepared = true + self + end + + def execute(*parms) + raise "The statement was not prepared" if @prepared == false + + if parms.size == 1 and parms[0].class == Array + parms = parms[0] + end + + if @numParms != parms.size + raise "Number of parameters supplied does not match with the SQL statement" + end + + if @numParms > 0 #need to bind parameters + #-------------------------------------------------------------------- + #calling bindParms may not be safe. Look comment below. + #-------------------------------------------------------------------- + #bindParms(parms) + + valueArray = [] + 1.upto(@numParms) do |i| # parameter number starts from 1 + type = @parmArray[i - 1].class + size = @parmArray[i - 1].size + decimalDigits = @parmArray[i - 1].decimalDigits + + if parms[i - 1].class == String + valueArray << parms[i - 1] + else + valueArray << parms[i - 1].to_s + end + + rc = SQLBindParameter(@handle, i, type, size, decimalDigits, valueArray[i - 1]) + check_rc(rc) + end + end + + check_rc(SQLExecute(@handle)) + + if @numParms != 0 + check_rc(SQLFreeStmt(@handle, SQL_RESET_PARAMS)) # Reset parameters + end + + self + end + + #------------------------------------------------------------------------------- + # The last argument(value) to SQLBindParameter is a deferred argument, that is, + # it should be available when SQLExecute is called. Even though "value" is + # local to bindParms method, it seems that it is available when SQLExecute + # is called. I am not sure whether it would still work if garbage collection + # is done between bindParms call and SQLExecute call inside the execute method + # above. + #------------------------------------------------------------------------------- + def bindParms(parms) # This is the real thing. It uses SQLBindParms + 1.upto(@numParms) do |i| # parameter number starts from 1 + rc, dataType, parmSize, decimalDigits = SQLDescribeParam(@handle, i) + check_rc(rc) + if parms[i - 1].class == String + value = parms[i - 1] + else + value = parms[i - 1].to_s + end + rc = SQLBindParameter(@handle, i, dataType, parmSize, decimalDigits, value) + check_rc(rc) + end + end + + #------------------------------------------------------------------------------ + # bind method does not use DB2's SQLBindParams, but replaces "?" in the + # SQL statement with the value before passing the SQL statement to DB2. + # It is not efficient and can handle only strings since it puts everything in + # quotes. + #------------------------------------------------------------------------------ + def bind(sql, args) #does not use SQLBindParams + arg_index = 0 + result = "" + tokens(sql).each do |part| + case part + when '?' + result << "'" + (args[arg_index]) + "'" #put it into quotes + arg_index += 1 + when '??' + result << "?" + else + result << part + end + end + if arg_index < args.size + raise "Too many SQL parameters" + elsif arg_index > args.size + raise "Not enough SQL parameters" + end + result + end + + ## Break the sql string into parts. + # + # This is NOT a full lexer for SQL. It just breaks up the SQL + # string enough so that question marks, double question marks and + # quoted strings are separated. This is used when binding + # arguments to "?" in the SQL string. Note: comments are not + # handled. + # + def tokens(sql) + toks = sql.scan(/('([^'\\]|''|\\.)*'|"([^"\\]|""|\\.)*"|\?\??|[^'"?]+)/) + toks.collect { |t| t[0] } + end + + def exec_direct(sql) + check_rc(SQLExecDirect(@handle, sql)) + self + end + + def set_cursor_name(name) + check_rc(SQLSetCursorName(@handle, name)) + self + end + + def get_cursor_name + rc, name = SQLGetCursorName(@handle) + check_rc(rc) + name + end + + def row_count + rc, rowcount = SQLRowCount(@handle) + check_rc(rc) + rowcount + end + + def num_result_cols + rc, cols = SQLNumResultCols(@handle) + check_rc(rc) + cols + end + + def fetch_all + if block_given? + while row = fetch do + yield row + end + else + res = [] + while row = fetch do + res << row + end + res + end + end + + def fetch + cols = get_col_desc + rc = SQLFetch(@handle) + if rc == SQL_NO_DATA_FOUND + SQLFreeStmt(@handle, SQL_CLOSE) # Close cursor + SQLFreeStmt(@handle, SQL_RESET_PARAMS) # Reset parameters + return nil + end + raise "ERROR" unless rc == SQL_SUCCESS + + retval = [] + cols.each_with_index do |c, i| + rc, content = SQLGetData(@handle, i + 1, c[1], c[2] + 1) #yun added 1 to c[2] + retval << adjust_content(content) + end + retval + end + + def fetch_as_hash + cols = get_col_desc + rc = SQLFetch(@handle) + if rc == SQL_NO_DATA_FOUND + SQLFreeStmt(@handle, SQL_CLOSE) # Close cursor + SQLFreeStmt(@handle, SQL_RESET_PARAMS) # Reset parameters + return nil + end + raise "ERROR" unless rc == SQL_SUCCESS + + retval = {} + cols.each_with_index do |c, i| + rc, content = SQLGetData(@handle, i + 1, c[1], c[2] + 1) #yun added 1 to c[2] + retval[c[0]] = adjust_content(content) + end + retval + end + + def get_col_desc + rc, nr_cols = SQLNumResultCols(@handle) + cols = (1..nr_cols).collect do |c| + rc, name, bl, type, col_sz = SQLDescribeCol(@handle, c, 1024) + [name.downcase, type, col_sz] + end + end + + def adjust_content(c) + case c.class.to_s + when 'DB2CLI::NullClass' + return nil + when 'DB2CLI::Time' + "%02d:%02d:%02d" % [c.hour, c.minute, c.second] + when 'DB2CLI::Date' + "%04d-%02d-%02d" % [c.year, c.month, c.day] + when 'DB2CLI::Timestamp' + "%04d-%02d-%02d %02d:%02d:%02d" % [c.year, c.month, c.day, c.hour, c.minute, c.second] + else + return c + end + end + end + + class Parameter + attr_reader :type, :size, :decimalDigits + def initialize(type, size, decimalDigits) + @type, @size, @decimalDigits = type, size, decimalDigits + end + end +end diff --git a/vendor/rails/activerecord/lib/active_record/vendor/mysql.rb b/vendor/rails/activerecord/lib/active_record/vendor/mysql.rb new file mode 100644 index 00000000..2599f433 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/vendor/mysql.rb @@ -0,0 +1,1195 @@ +# $Id: mysql.rb,v 1.24 2005/02/12 11:37:15 tommy Exp $ +# +# Copyright (C) 2003-2005 TOMITA Masahiro +# tommy@tmtm.org +# + +class Mysql + + VERSION = "4.0-ruby-0.2.5" + + require "socket" + require "digest/sha1" + + MAX_PACKET_LENGTH = 256*256*256-1 + MAX_ALLOWED_PACKET = 1024*1024*1024 + + MYSQL_UNIX_ADDR = "/tmp/mysql.sock" + MYSQL_PORT = 3306 + PROTOCOL_VERSION = 10 + + # Command + COM_SLEEP = 0 + COM_QUIT = 1 + COM_INIT_DB = 2 + COM_QUERY = 3 + COM_FIELD_LIST = 4 + COM_CREATE_DB = 5 + COM_DROP_DB = 6 + COM_REFRESH = 7 + COM_SHUTDOWN = 8 + COM_STATISTICS = 9 + COM_PROCESS_INFO = 10 + COM_CONNECT = 11 + COM_PROCESS_KILL = 12 + COM_DEBUG = 13 + COM_PING = 14 + COM_TIME = 15 + COM_DELAYED_INSERT = 16 + COM_CHANGE_USER = 17 + COM_BINLOG_DUMP = 18 + COM_TABLE_DUMP = 19 + COM_CONNECT_OUT = 20 + COM_REGISTER_SLAVE = 21 + + # Client flag + CLIENT_LONG_PASSWORD = 1 + CLIENT_FOUND_ROWS = 1 << 1 + CLIENT_LONG_FLAG = 1 << 2 + CLIENT_CONNECT_WITH_DB= 1 << 3 + CLIENT_NO_SCHEMA = 1 << 4 + CLIENT_COMPRESS = 1 << 5 + CLIENT_ODBC = 1 << 6 + CLIENT_LOCAL_FILES = 1 << 7 + CLIENT_IGNORE_SPACE = 1 << 8 + CLIENT_PROTOCOL_41 = 1 << 9 + CLIENT_INTERACTIVE = 1 << 10 + CLIENT_SSL = 1 << 11 + CLIENT_IGNORE_SIGPIPE = 1 << 12 + CLIENT_TRANSACTIONS = 1 << 13 + CLIENT_RESERVED = 1 << 14 + CLIENT_SECURE_CONNECTION = 1 << 15 + CLIENT_CAPABILITIES = CLIENT_LONG_PASSWORD|CLIENT_LONG_FLAG|CLIENT_TRANSACTIONS + PROTO_AUTH41 = CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION + + # Connection Option + OPT_CONNECT_TIMEOUT = 0 + OPT_COMPRESS = 1 + OPT_NAMED_PIPE = 2 + INIT_COMMAND = 3 + READ_DEFAULT_FILE = 4 + READ_DEFAULT_GROUP = 5 + SET_CHARSET_DIR = 6 + SET_CHARSET_NAME = 7 + OPT_LOCAL_INFILE = 8 + + # Server Status + SERVER_STATUS_IN_TRANS = 1 + SERVER_STATUS_AUTOCOMMIT = 2 + + # Refresh parameter + REFRESH_GRANT = 1 + REFRESH_LOG = 2 + REFRESH_TABLES = 4 + REFRESH_HOSTS = 8 + REFRESH_STATUS = 16 + REFRESH_THREADS = 32 + REFRESH_SLAVE = 64 + REFRESH_MASTER = 128 + + def initialize(*args) + @client_flag = 0 + @max_allowed_packet = MAX_ALLOWED_PACKET + @query_with_result = true + @status = :STATUS_READY + if args[0] != :INIT then + real_connect(*args) + end + end + + def real_connect(host=nil, user=nil, passwd=nil, db=nil, port=nil, socket=nil, flag=nil) + @server_status = SERVER_STATUS_AUTOCOMMIT + if (host == nil or host == "localhost") and defined? UNIXSocket then + unix_socket = socket || ENV["MYSQL_UNIX_PORT"] || MYSQL_UNIX_ADDR + sock = UNIXSocket::new(unix_socket) + @host_info = Error::err(Error::CR_LOCALHOST_CONNECTION) + @unix_socket = unix_socket + else + sock = TCPSocket::new(host, port||ENV["MYSQL_TCP_PORT"]||(Socket::getservbyname("mysql","tcp") rescue MYSQL_PORT)) + @host_info = sprintf Error::err(Error::CR_TCP_CONNECTION), host + end + @host = host ? host.dup : nil + sock.setsockopt Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true + @net = Net::new sock + + a = read + @protocol_version = a.slice!(0) + @server_version, a = a.split(/\0/,2) + @thread_id, @scramble_buff = a.slice!(0,13).unpack("La8") + if a.size >= 2 then + @server_capabilities, = a.slice!(0,2).unpack("v") + end + if a.size >= 16 then + @server_language, @server_status = a.slice!(0,3).unpack("cv") + end + + flag = 0 if flag == nil + flag |= @client_flag | CLIENT_CAPABILITIES + flag |= CLIENT_CONNECT_WITH_DB if db + + @pre_411 = (0 == @server_capabilities & PROTO_AUTH41) + if @pre_411 + data = Net::int2str(flag)+Net::int3str(@max_allowed_packet)+ + (user||"")+"\0"+ + scramble(passwd, @scramble_buff, @protocol_version==9) + else + dummy, @salt2 = a.unpack("a13a12") + @scramble_buff += @salt2 + flag |= PROTO_AUTH41 + data = Net::int4str(flag) + Net::int4str(@max_allowed_packet) + + ([8] + Array.new(23, 0)).pack("c24") + (user||"")+"\0"+ + scramble41(passwd, @scramble_buff) + end + + if db and @server_capabilities & CLIENT_CONNECT_WITH_DB != 0 + data << "\0" if @pre_411 + data << db + @db = db.dup + end + write data + read + ObjectSpace.define_finalizer(self, Mysql.finalizer(@net)) + self + end + alias :connect :real_connect + + def escape_string(str) + Mysql::escape_string str + end + alias :quote :escape_string + + def get_client_info() + VERSION + end + alias :client_info :get_client_info + + def options(option, arg=nil) + if option == OPT_LOCAL_INFILE then + if arg == false or arg == 0 then + @client_flag &= ~CLIENT_LOCAL_FILES + else + @client_flag |= CLIENT_LOCAL_FILES + end + else + raise "not implemented" + end + end + + def real_query(query) + command COM_QUERY, query, true + read_query_result + self + end + + def use_result() + if @status != :STATUS_GET_RESULT then + error Error::CR_COMMANDS_OUT_OF_SYNC + end + res = Result::new self, @fields, @field_count + @status = :STATUS_USE_RESULT + res + end + + def store_result() + if @status != :STATUS_GET_RESULT then + error Error::CR_COMMANDS_OUT_OF_SYNC + end + @status = :STATUS_READY + data = read_rows @field_count + res = Result::new self, @fields, @field_count, data + @fields = nil + @affected_rows = data.length + res + end + + def change_user(user="", passwd="", db="") + if @pre_411 + data = user+"\0"+scramble(passwd, @scramble_buff, @protocol_version==9)+"\0"+db + else + data = user+"\0"+scramble41(passwd, @scramble_buff)+db + end + command COM_CHANGE_USER, data + @user = user + @passwd = passwd + @db = db + end + + def character_set_name() + raise "not implemented" + end + + def close() + @status = :STATUS_READY + command COM_QUIT, nil, true + @net.close + self + end + + def create_db(db) + command COM_CREATE_DB, db + self + end + + def drop_db(db) + command COM_DROP_DB, db + self + end + + def dump_debug_info() + command COM_DEBUG + self + end + + def get_host_info() + @host_info + end + alias :host_info :get_host_info + + def get_proto_info() + @protocol_version + end + alias :proto_info :get_proto_info + + def get_server_info() + @server_version + end + alias :server_info :get_server_info + + def kill(id) + command COM_PROCESS_KILL, Net::int4str(id) + self + end + + def list_dbs(db=nil) + real_query "show databases #{db}" + @status = :STATUS_READY + read_rows(1).flatten + end + + def list_fields(table, field=nil) + command COM_FIELD_LIST, "#{table}\0#{field}", true + if @pre_411 + f = read_rows 6 + else + f = read_rows 7 + end + fields = unpack_fields(f, @server_capabilities & CLIENT_LONG_FLAG != 0) + res = Result::new self, fields, f.length + res.eof = true + res + end + + def list_processes() + data = command COM_PROCESS_INFO + @field_count = get_length data + if @pre_411 + fields = read_rows 5 + else + fields = read_rows 7 + end + @fields = unpack_fields(fields, @server_capabilities & CLIENT_LONG_FLAG != 0) + @status = :STATUS_GET_RESULT + store_result + end + + def list_tables(table=nil) + real_query "show tables #{table}" + @status = :STATUS_READY + read_rows(1).flatten + end + + def ping() + command COM_PING + self + end + + def query(query) + real_query query + if not @query_with_result then + return self + end + if @field_count == 0 then + return nil + end + store_result + end + + def refresh(r) + command COM_REFRESH, r.chr + self + end + + def reload() + refresh REFRESH_GRANT + self + end + + def select_db(db) + command COM_INIT_DB, db + @db = db + self + end + + def shutdown() + command COM_SHUTDOWN + self + end + + def stat() + command COM_STATISTICS + end + + attr_reader :info, :insert_id, :affected_rows, :field_count, :thread_id + attr_accessor :query_with_result, :status + + def read_one_row(field_count) + data = read + if data[0] == 254 and data.length == 1 ## EOF + return + elsif data[0] == 254 and data.length == 5 + return + end + rec = [] + field_count.times do + len = get_length data + if len == nil then + rec << len + else + rec << data.slice!(0,len) + end + end + rec + end + + def skip_result() + if @status == :STATUS_USE_RESULT then + loop do + data = read + break if data[0] == 254 and data.length == 1 + end + @status = :STATUS_READY + end + end + + def inspect() + "#<#{self.class}>" + end + + private + + def read_query_result() + data = read + @field_count = get_length(data) + if @field_count == nil then # LOAD DATA LOCAL INFILE + File::open(data) do |f| + write f.read + end + write "" # mark EOF + data = read + @field_count = get_length(data) + end + if @field_count == 0 then + @affected_rows = get_length(data, true) + @insert_id = get_length(data, true) + if @server_capabilities & CLIENT_TRANSACTIONS != 0 then + a = data.slice!(0,2) + @server_status = a[0]+a[1]*256 + end + if data.size > 0 and get_length(data) then + @info = data + end + else + @extra_info = get_length(data, true) + if @pre_411 + fields = read_rows(5) + else + fields = read_rows(7) + end + @fields = unpack_fields(fields, @server_capabilities & CLIENT_LONG_FLAG != 0) + @status = :STATUS_GET_RESULT + end + self + end + + def unpack_fields(data, long_flag_protocol) + ret = [] + data.each do |f| + if @pre_411 + table = org_table = f[0] + name = f[1] + length = f[2][0]+f[2][1]*256+f[2][2]*256*256 + type = f[3][0] + if long_flag_protocol then + flags = f[4][0]+f[4][1]*256 + decimals = f[4][2] + else + flags = f[4][0] + decimals = f[4][1] + end + def_value = f[5] + max_length = 0 + else + catalog = f[0] + db = f[1] + table = f[2] + org_table = f[3] + name = f[4] + org_name = f[5] + length = f[6][2]+f[6][3]*256+f[6][4]*256*256 + type = f[6][6] + flags = f[6][7]+f[6][8]*256 + decimals = f[6][9] + def_value = "" + max_length = 0 + end + ret << Field::new(table, org_table, name, length, type, flags, decimals, def_value, max_length) + end + ret + end + + def read_rows(field_count) + ret = [] + while rec = read_one_row(field_count) do + ret << rec + end + ret + end + + def get_length(data, longlong=nil) + return if data.length == 0 + c = data.slice!(0) + case c + when 251 + return nil + when 252 + a = data.slice!(0,2) + return a[0]+a[1]*256 + when 253 + a = data.slice!(0,3) + return a[0]+a[1]*256+a[2]*256**2 + when 254 + a = data.slice!(0,8) + if longlong then + return a[0]+a[1]*256+a[2]*256**2+a[3]*256**3+ + a[4]*256**4+a[5]*256**5+a[6]*256**6+a[7]*256**7 + else + return a[0]+a[1]*256+a[2]*256**2+a[3]*256**3 + end + else + c + end + end + + def command(cmd, arg=nil, skip_check=nil) + unless @net then + error Error::CR_SERVER_GONE_ERROR + end + if @status != :STATUS_READY then + error Error::CR_COMMANDS_OUT_OF_SYNC + end + @net.clear + write cmd.chr+(arg||"") + read unless skip_check + end + + def read() + unless @net then + error Error::CR_SERVER_GONE_ERROR + end + a = @net.read + if a[0] == 255 then + if a.length > 3 then + @errno = a[1]+a[2]*256 + @error = a[3 .. -1] + else + @errno = Error::CR_UNKNOWN_ERROR + @error = Error::err @errno + end + raise Error::new(@errno, @error) + end + a + end + + def write(arg) + unless @net then + error Error::CR_SERVER_GONE_ERROR + end + @net.write arg + end + + def hash_password(password) + nr = 1345345333 + add = 7 + nr2 = 0x12345671 + password.each_byte do |i| + next if i == 0x20 or i == 9 + nr ^= (((nr & 63) + add) * i) + (nr << 8) + nr2 += (nr2 << 8) ^ nr + add += i + end + [nr & ((1 << 31) - 1), nr2 & ((1 << 31) - 1)] + end + + def scramble(password, message, old_ver) + return "" if password == nil or password == "" + raise "old version password is not implemented" if old_ver + hash_pass = hash_password password + hash_message = hash_password message + rnd = Random::new hash_pass[0] ^ hash_message[0], hash_pass[1] ^ hash_message[1] + to = [] + 1.upto(message.length) do + to << ((rnd.rnd*31)+64).floor + end + extra = (rnd.rnd*31).floor + to.map! do |t| (t ^ extra).chr end + to.join + end + + def scramble41(password, message) + return 0x00.chr if password.nil? or password.empty? + buf = [0x14] + s1 = Digest::SHA1.new(password).digest + s2 = Digest::SHA1.new(s1).digest + x = Digest::SHA1.new(message + s2).digest + (0..s1.length - 1).each {|i| buf.push(s1[i] ^ x[i])} + buf.pack("C*") + end + + def error(errno) + @errno = errno + @error = Error::err errno + raise Error::new(@errno, @error) + end + + class Result + def initialize(mysql, fields, field_count, data=nil) + @handle = mysql + @fields = fields + @field_count = field_count + @data = data + @current_field = 0 + @current_row = 0 + @eof = false + @row_count = 0 + end + attr_accessor :eof + + def data_seek(n) + @current_row = n + end + + def fetch_field() + return if @current_field >= @field_count + f = @fields[@current_field] + @current_field += 1 + f + end + + def fetch_fields() + @fields + end + + def fetch_field_direct(n) + @fields[n] + end + + def fetch_lengths() + @data ? @data[@current_row].map{|i| i ? i.length : 0} : @lengths + end + + def fetch_row() + if @data then + if @current_row >= @data.length then + @handle.status = :STATUS_READY + return + end + ret = @data[@current_row] + @current_row += 1 + else + return if @eof + ret = @handle.read_one_row @field_count + if ret == nil then + @eof = true + return + end + @lengths = ret.map{|i| i ? i.length : 0} + @row_count += 1 + end + ret + end + + def fetch_hash(with_table=nil) + row = fetch_row + return if row == nil + hash = {} + @fields.each_index do |i| + f = with_table ? @fields[i].table+"."+@fields[i].name : @fields[i].name + hash[f] = row[i] + end + hash + end + + def field_seek(n) + @current_field = n + end + + def field_tell() + @current_field + end + + def free() + @handle.skip_result + @handle = @fields = @data = nil + end + + def num_fields() + @field_count + end + + def num_rows() + @data ? @data.length : @row_count + end + + def row_seek(n) + @current_row = n + end + + def row_tell() + @current_row + end + + def each() + while row = fetch_row do + yield row + end + end + + def each_hash(with_table=nil) + while hash = fetch_hash(with_table) do + yield hash + end + end + + def inspect() + "#<#{self.class}>" + end + + end + + class Field + # Field type + TYPE_DECIMAL = 0 + TYPE_TINY = 1 + TYPE_SHORT = 2 + TYPE_LONG = 3 + TYPE_FLOAT = 4 + TYPE_DOUBLE = 5 + TYPE_NULL = 6 + TYPE_TIMESTAMP = 7 + TYPE_LONGLONG = 8 + TYPE_INT24 = 9 + TYPE_DATE = 10 + TYPE_TIME = 11 + TYPE_DATETIME = 12 + TYPE_YEAR = 13 + TYPE_NEWDATE = 14 + TYPE_ENUM = 247 + TYPE_SET = 248 + TYPE_TINY_BLOB = 249 + TYPE_MEDIUM_BLOB = 250 + TYPE_LONG_BLOB = 251 + TYPE_BLOB = 252 + TYPE_VAR_STRING = 253 + TYPE_STRING = 254 + TYPE_GEOMETRY = 255 + TYPE_CHAR = TYPE_TINY + TYPE_INTERVAL = TYPE_ENUM + + # Flag + NOT_NULL_FLAG = 1 + PRI_KEY_FLAG = 2 + UNIQUE_KEY_FLAG = 4 + MULTIPLE_KEY_FLAG = 8 + BLOB_FLAG = 16 + UNSIGNED_FLAG = 32 + ZEROFILL_FLAG = 64 + BINARY_FLAG = 128 + ENUM_FLAG = 256 + AUTO_INCREMENT_FLAG = 512 + TIMESTAMP_FLAG = 1024 + SET_FLAG = 2048 + NUM_FLAG = 32768 + PART_KEY_FLAG = 16384 + GROUP_FLAG = 32768 + UNIQUE_FLAG = 65536 + + def initialize(table, org_table, name, length, type, flags, decimals, def_value, max_length) + @table = table + @org_table = org_table + @name = name + @length = length + @type = type + @flags = flags + @decimals = decimals + @def = def_value + @max_length = max_length + if (type <= TYPE_INT24 and (type != TYPE_TIMESTAMP or length == 14 or length == 8)) or type == TYPE_YEAR then + @flags |= NUM_FLAG + end + end + attr_reader :table, :org_table, :name, :length, :type, :flags, :decimals, :def, :max_length + + def inspect() + "#<#{self.class}:#{@name}>" + end + end + + class Error < StandardError + # Server Error + ER_HASHCHK = 1000 + ER_NISAMCHK = 1001 + ER_NO = 1002 + ER_YES = 1003 + ER_CANT_CREATE_FILE = 1004 + ER_CANT_CREATE_TABLE = 1005 + ER_CANT_CREATE_DB = 1006 + ER_DB_CREATE_EXISTS = 1007 + ER_DB_DROP_EXISTS = 1008 + ER_DB_DROP_DELETE = 1009 + ER_DB_DROP_RMDIR = 1010 + ER_CANT_DELETE_FILE = 1011 + ER_CANT_FIND_SYSTEM_REC = 1012 + ER_CANT_GET_STAT = 1013 + ER_CANT_GET_WD = 1014 + ER_CANT_LOCK = 1015 + ER_CANT_OPEN_FILE = 1016 + ER_FILE_NOT_FOUND = 1017 + ER_CANT_READ_DIR = 1018 + ER_CANT_SET_WD = 1019 + ER_CHECKREAD = 1020 + ER_DISK_FULL = 1021 + ER_DUP_KEY = 1022 + ER_ERROR_ON_CLOSE = 1023 + ER_ERROR_ON_READ = 1024 + ER_ERROR_ON_RENAME = 1025 + ER_ERROR_ON_WRITE = 1026 + ER_FILE_USED = 1027 + ER_FILSORT_ABORT = 1028 + ER_FORM_NOT_FOUND = 1029 + ER_GET_ERRNO = 1030 + ER_ILLEGAL_HA = 1031 + ER_KEY_NOT_FOUND = 1032 + ER_NOT_FORM_FILE = 1033 + ER_NOT_KEYFILE = 1034 + ER_OLD_KEYFILE = 1035 + ER_OPEN_AS_READONLY = 1036 + ER_OUTOFMEMORY = 1037 + ER_OUT_OF_SORTMEMORY = 1038 + ER_UNEXPECTED_EOF = 1039 + ER_CON_COUNT_ERROR = 1040 + ER_OUT_OF_RESOURCES = 1041 + ER_BAD_HOST_ERROR = 1042 + ER_HANDSHAKE_ERROR = 1043 + ER_DBACCESS_DENIED_ERROR = 1044 + ER_ACCESS_DENIED_ERROR = 1045 + ER_NO_DB_ERROR = 1046 + ER_UNKNOWN_COM_ERROR = 1047 + ER_BAD_NULL_ERROR = 1048 + ER_BAD_DB_ERROR = 1049 + ER_TABLE_EXISTS_ERROR = 1050 + ER_BAD_TABLE_ERROR = 1051 + ER_NON_UNIQ_ERROR = 1052 + ER_SERVER_SHUTDOWN = 1053 + ER_BAD_FIELD_ERROR = 1054 + ER_WRONG_FIELD_WITH_GROUP = 1055 + ER_WRONG_GROUP_FIELD = 1056 + ER_WRONG_SUM_SELECT = 1057 + ER_WRONG_VALUE_COUNT = 1058 + ER_TOO_LONG_IDENT = 1059 + ER_DUP_FIELDNAME = 1060 + ER_DUP_KEYNAME = 1061 + ER_DUP_ENTRY = 1062 + ER_WRONG_FIELD_SPEC = 1063 + ER_PARSE_ERROR = 1064 + ER_EMPTY_QUERY = 1065 + ER_NONUNIQ_TABLE = 1066 + ER_INVALID_DEFAULT = 1067 + ER_MULTIPLE_PRI_KEY = 1068 + ER_TOO_MANY_KEYS = 1069 + ER_TOO_MANY_KEY_PARTS = 1070 + ER_TOO_LONG_KEY = 1071 + ER_KEY_COLUMN_DOES_NOT_EXITS = 1072 + ER_BLOB_USED_AS_KEY = 1073 + ER_TOO_BIG_FIELDLENGTH = 1074 + ER_WRONG_AUTO_KEY = 1075 + ER_READY = 1076 + ER_NORMAL_SHUTDOWN = 1077 + ER_GOT_SIGNAL = 1078 + ER_SHUTDOWN_COMPLETE = 1079 + ER_FORCING_CLOSE = 1080 + ER_IPSOCK_ERROR = 1081 + ER_NO_SUCH_INDEX = 1082 + ER_WRONG_FIELD_TERMINATORS = 1083 + ER_BLOBS_AND_NO_TERMINATED = 1084 + ER_TEXTFILE_NOT_READABLE = 1085 + ER_FILE_EXISTS_ERROR = 1086 + ER_LOAD_INFO = 1087 + ER_ALTER_INFO = 1088 + ER_WRONG_SUB_KEY = 1089 + ER_CANT_REMOVE_ALL_FIELDS = 1090 + ER_CANT_DROP_FIELD_OR_KEY = 1091 + ER_INSERT_INFO = 1092 + ER_INSERT_TABLE_USED = 1093 + ER_NO_SUCH_THREAD = 1094 + ER_KILL_DENIED_ERROR = 1095 + ER_NO_TABLES_USED = 1096 + ER_TOO_BIG_SET = 1097 + ER_NO_UNIQUE_LOGFILE = 1098 + ER_TABLE_NOT_LOCKED_FOR_WRITE = 1099 + ER_TABLE_NOT_LOCKED = 1100 + ER_BLOB_CANT_HAVE_DEFAULT = 1101 + ER_WRONG_DB_NAME = 1102 + ER_WRONG_TABLE_NAME = 1103 + ER_TOO_BIG_SELECT = 1104 + ER_UNKNOWN_ERROR = 1105 + ER_UNKNOWN_PROCEDURE = 1106 + ER_WRONG_PARAMCOUNT_TO_PROCEDURE = 1107 + ER_WRONG_PARAMETERS_TO_PROCEDURE = 1108 + ER_UNKNOWN_TABLE = 1109 + ER_FIELD_SPECIFIED_TWICE = 1110 + ER_INVALID_GROUP_FUNC_USE = 1111 + ER_UNSUPPORTED_EXTENSION = 1112 + ER_TABLE_MUST_HAVE_COLUMNS = 1113 + ER_RECORD_FILE_FULL = 1114 + ER_UNKNOWN_CHARACTER_SET = 1115 + ER_TOO_MANY_TABLES = 1116 + ER_TOO_MANY_FIELDS = 1117 + ER_TOO_BIG_ROWSIZE = 1118 + ER_STACK_OVERRUN = 1119 + ER_WRONG_OUTER_JOIN = 1120 + ER_NULL_COLUMN_IN_INDEX = 1121 + ER_CANT_FIND_UDF = 1122 + ER_CANT_INITIALIZE_UDF = 1123 + ER_UDF_NO_PATHS = 1124 + ER_UDF_EXISTS = 1125 + ER_CANT_OPEN_LIBRARY = 1126 + ER_CANT_FIND_DL_ENTRY = 1127 + ER_FUNCTION_NOT_DEFINED = 1128 + ER_HOST_IS_BLOCKED = 1129 + ER_HOST_NOT_PRIVILEGED = 1130 + ER_PASSWORD_ANONYMOUS_USER = 1131 + ER_PASSWORD_NOT_ALLOWED = 1132 + ER_PASSWORD_NO_MATCH = 1133 + ER_UPDATE_INFO = 1134 + ER_CANT_CREATE_THREAD = 1135 + ER_WRONG_VALUE_COUNT_ON_ROW = 1136 + ER_CANT_REOPEN_TABLE = 1137 + ER_INVALID_USE_OF_NULL = 1138 + ER_REGEXP_ERROR = 1139 + ER_MIX_OF_GROUP_FUNC_AND_FIELDS = 1140 + ER_NONEXISTING_GRANT = 1141 + ER_TABLEACCESS_DENIED_ERROR = 1142 + ER_COLUMNACCESS_DENIED_ERROR = 1143 + ER_ILLEGAL_GRANT_FOR_TABLE = 1144 + ER_GRANT_WRONG_HOST_OR_USER = 1145 + ER_NO_SUCH_TABLE = 1146 + ER_NONEXISTING_TABLE_GRANT = 1147 + ER_NOT_ALLOWED_COMMAND = 1148 + ER_SYNTAX_ERROR = 1149 + ER_DELAYED_CANT_CHANGE_LOCK = 1150 + ER_TOO_MANY_DELAYED_THREADS = 1151 + ER_ABORTING_CONNECTION = 1152 + ER_NET_PACKET_TOO_LARGE = 1153 + ER_NET_READ_ERROR_FROM_PIPE = 1154 + ER_NET_FCNTL_ERROR = 1155 + ER_NET_PACKETS_OUT_OF_ORDER = 1156 + ER_NET_UNCOMPRESS_ERROR = 1157 + ER_NET_READ_ERROR = 1158 + ER_NET_READ_INTERRUPTED = 1159 + ER_NET_ERROR_ON_WRITE = 1160 + ER_NET_WRITE_INTERRUPTED = 1161 + ER_TOO_LONG_STRING = 1162 + ER_TABLE_CANT_HANDLE_BLOB = 1163 + ER_TABLE_CANT_HANDLE_AUTO_INCREMENT = 1164 + ER_DELAYED_INSERT_TABLE_LOCKED = 1165 + ER_WRONG_COLUMN_NAME = 1166 + ER_WRONG_KEY_COLUMN = 1167 + ER_WRONG_MRG_TABLE = 1168 + ER_DUP_UNIQUE = 1169 + ER_BLOB_KEY_WITHOUT_LENGTH = 1170 + ER_PRIMARY_CANT_HAVE_NULL = 1171 + ER_TOO_MANY_ROWS = 1172 + ER_REQUIRES_PRIMARY_KEY = 1173 + ER_NO_RAID_COMPILED = 1174 + ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE = 1175 + ER_KEY_DOES_NOT_EXITS = 1176 + ER_CHECK_NO_SUCH_TABLE = 1177 + ER_CHECK_NOT_IMPLEMENTED = 1178 + ER_CANT_DO_THIS_DURING_AN_TRANSACTION = 1179 + ER_ERROR_DURING_COMMIT = 1180 + ER_ERROR_DURING_ROLLBACK = 1181 + ER_ERROR_DURING_FLUSH_LOGS = 1182 + ER_ERROR_DURING_CHECKPOINT = 1183 + ER_NEW_ABORTING_CONNECTION = 1184 + ER_DUMP_NOT_IMPLEMENTED = 1185 + ER_FLUSH_MASTER_BINLOG_CLOSED = 1186 + ER_INDEX_REBUILD = 1187 + ER_MASTER = 1188 + ER_MASTER_NET_READ = 1189 + ER_MASTER_NET_WRITE = 1190 + ER_FT_MATCHING_KEY_NOT_FOUND = 1191 + ER_LOCK_OR_ACTIVE_TRANSACTION = 1192 + ER_UNKNOWN_SYSTEM_VARIABLE = 1193 + ER_CRASHED_ON_USAGE = 1194 + ER_CRASHED_ON_REPAIR = 1195 + ER_WARNING_NOT_COMPLETE_ROLLBACK = 1196 + ER_TRANS_CACHE_FULL = 1197 + ER_SLAVE_MUST_STOP = 1198 + ER_SLAVE_NOT_RUNNING = 1199 + ER_BAD_SLAVE = 1200 + ER_MASTER_INFO = 1201 + ER_SLAVE_THREAD = 1202 + ER_TOO_MANY_USER_CONNECTIONS = 1203 + ER_SET_CONSTANTS_ONLY = 1204 + ER_LOCK_WAIT_TIMEOUT = 1205 + ER_LOCK_TABLE_FULL = 1206 + ER_READ_ONLY_TRANSACTION = 1207 + ER_DROP_DB_WITH_READ_LOCK = 1208 + ER_CREATE_DB_WITH_READ_LOCK = 1209 + ER_WRONG_ARGUMENTS = 1210 + ER_NO_PERMISSION_TO_CREATE_USER = 1211 + ER_UNION_TABLES_IN_DIFFERENT_DIR = 1212 + ER_LOCK_DEADLOCK = 1213 + ER_TABLE_CANT_HANDLE_FULLTEXT = 1214 + ER_CANNOT_ADD_FOREIGN = 1215 + ER_NO_REFERENCED_ROW = 1216 + ER_ROW_IS_REFERENCED = 1217 + ER_CONNECT_TO_MASTER = 1218 + ER_QUERY_ON_MASTER = 1219 + ER_ERROR_WHEN_EXECUTING_COMMAND = 1220 + ER_WRONG_USAGE = 1221 + ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT = 1222 + ER_CANT_UPDATE_WITH_READLOCK = 1223 + ER_MIXING_NOT_ALLOWED = 1224 + ER_DUP_ARGUMENT = 1225 + ER_USER_LIMIT_REACHED = 1226 + ER_SPECIFIC_ACCESS_DENIED_ERROR = 1227 + ER_LOCAL_VARIABLE = 1228 + ER_GLOBAL_VARIABLE = 1229 + ER_NO_DEFAULT = 1230 + ER_WRONG_VALUE_FOR_VAR = 1231 + ER_WRONG_TYPE_FOR_VAR = 1232 + ER_VAR_CANT_BE_READ = 1233 + ER_CANT_USE_OPTION_HERE = 1234 + ER_NOT_SUPPORTED_YET = 1235 + ER_MASTER_FATAL_ERROR_READING_BINLOG = 1236 + ER_SLAVE_IGNORED_TABLE = 1237 + ER_ERROR_MESSAGES = 238 + + # Client Error + CR_MIN_ERROR = 2000 + CR_MAX_ERROR = 2999 + CR_UNKNOWN_ERROR = 2000 + CR_SOCKET_CREATE_ERROR = 2001 + CR_CONNECTION_ERROR = 2002 + CR_CONN_HOST_ERROR = 2003 + CR_IPSOCK_ERROR = 2004 + CR_UNKNOWN_HOST = 2005 + CR_SERVER_GONE_ERROR = 2006 + CR_VERSION_ERROR = 2007 + CR_OUT_OF_MEMORY = 2008 + CR_WRONG_HOST_INFO = 2009 + CR_LOCALHOST_CONNECTION = 2010 + CR_TCP_CONNECTION = 2011 + CR_SERVER_HANDSHAKE_ERR = 2012 + CR_SERVER_LOST = 2013 + CR_COMMANDS_OUT_OF_SYNC = 2014 + CR_NAMEDPIPE_CONNECTION = 2015 + CR_NAMEDPIPEWAIT_ERROR = 2016 + CR_NAMEDPIPEOPEN_ERROR = 2017 + CR_NAMEDPIPESETSTATE_ERROR = 2018 + CR_CANT_READ_CHARSET = 2019 + CR_NET_PACKET_TOO_LARGE = 2020 + CR_EMBEDDED_CONNECTION = 2021 + CR_PROBE_SLAVE_STATUS = 2022 + CR_PROBE_SLAVE_HOSTS = 2023 + CR_PROBE_SLAVE_CONNECT = 2024 + CR_PROBE_MASTER_CONNECT = 2025 + CR_SSL_CONNECTION_ERROR = 2026 + CR_MALFORMED_PACKET = 2027 + + CLIENT_ERRORS = [ + "Unknown MySQL error", + "Can't create UNIX socket (%d)", + "Can't connect to local MySQL server through socket '%-.64s' (%d)", + "Can't connect to MySQL server on '%-.64s' (%d)", + "Can't create TCP/IP socket (%d)", + "Unknown MySQL Server Host '%-.64s' (%d)", + "MySQL server has gone away", + "Protocol mismatch. Server Version = %d Client Version = %d", + "MySQL client run out of memory", + "Wrong host info", + "Localhost via UNIX socket", + "%-.64s via TCP/IP", + "Error in server handshake", + "Lost connection to MySQL server during query", + "Commands out of sync; You can't run this command now", + "%-.64s via named pipe", + "Can't wait for named pipe to host: %-.64s pipe: %-.32s (%lu)", + "Can't open named pipe to host: %-.64s pipe: %-.32s (%lu)", + "Can't set state of named pipe to host: %-.64s pipe: %-.32s (%lu)", + "Can't initialize character set %-.64s (path: %-.64s)", + "Got packet bigger than 'max_allowed_packet'", + "Embedded server", + "Error on SHOW SLAVE STATUS:", + "Error on SHOW SLAVE HOSTS:", + "Error connecting to slave:", + "Error connecting to master:", + "SSL connection error", + "Malformed packet" + ] + + def initialize(errno, error) + @errno = errno + @error = error + super error + end + attr_reader :errno, :error + + def Error::err(errno) + CLIENT_ERRORS[errno - Error::CR_MIN_ERROR] + end + end + + class Net + def initialize(sock) + @sock = sock + @pkt_nr = 0 + end + + def clear() + @pkt_nr = 0 + end + + def read() + buf = [] + len = nil + @sock.sync = false + while len == nil or len == MAX_PACKET_LENGTH do + a = @sock.read(4) + len = a[0]+a[1]*256+a[2]*256*256 + pkt_nr = a[3] + if @pkt_nr != pkt_nr then + raise "Packets out of order: #{@pkt_nr}<>#{pkt_nr}" + end + @pkt_nr = @pkt_nr + 1 & 0xff + buf << @sock.read(len) + end + @sock.sync = true + buf.join + rescue + errno = Error::CR_SERVER_LOST + raise Error::new(errno, Error::err(errno)) + end + + def write(data) + if data.is_a? Array then + data = data.join + end + @sock.sync = false + ptr = 0 + while data.length >= MAX_PACKET_LENGTH do + @sock.write Net::int3str(MAX_PACKET_LENGTH)+@pkt_nr.chr+data[ptr, MAX_PACKET_LENGTH] + @pkt_nr = @pkt_nr + 1 & 0xff + ptr += MAX_PACKET_LENGTH + end + @sock.write Net::int3str(data.length-ptr)+@pkt_nr.chr+data[ptr .. -1] + @pkt_nr = @pkt_nr + 1 & 0xff + @sock.sync = true + @sock.flush + rescue + errno = Error::CR_SERVER_LOST + raise Error::new(errno, Error::err(errno)) + end + + def close() + @sock.close + end + + def Net::int2str(n) + [n].pack("v") + end + + def Net::int3str(n) + [n%256, n>>8].pack("cv") + end + + def Net::int4str(n) + [n].pack("V") + end + + end + + class Random + def initialize(seed1, seed2) + @max_value = 0x3FFFFFFF + @seed1 = seed1 % @max_value + @seed2 = seed2 % @max_value + end + + def rnd() + @seed1 = (@seed1*3+@seed2) % @max_value + @seed2 = (@seed1+@seed2+33) % @max_value + @seed1.to_f / @max_value + end + end + +end + +class << Mysql + def init() + Mysql::new :INIT + end + + def real_connect(*args) + Mysql::new(*args) + end + alias :connect :real_connect + + def finalizer(net) + proc { + net.clear + net.write Mysql::COM_QUIT.chr + } + end + + def escape_string(str) + str.gsub(/([\0\n\r\032\'\"\\])/) do + case $1 + when "\0" then "\\0" + when "\n" then "\\n" + when "\r" then "\\r" + when "\032" then "\\Z" + else "\\"+$1 + end + end + end + alias :quote :escape_string + + def get_client_info() + Mysql::VERSION + end + alias :client_info :get_client_info + + def debug(str) + raise "not implemented" + end +end + +# +# for compatibility +# + +MysqlRes = Mysql::Result +MysqlField = Mysql::Field +MysqlError = Mysql::Error diff --git a/vendor/rails/activerecord/lib/active_record/vendor/simple.rb b/vendor/rails/activerecord/lib/active_record/vendor/simple.rb new file mode 100644 index 00000000..7ac3cd08 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/vendor/simple.rb @@ -0,0 +1,693 @@ +# :title: Transaction::Simple -- Active Object Transaction Support for Ruby +# :main: Transaction::Simple +# +# == Licence +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +#-- +# Transaction::Simple +# Simple object transaction support for Ruby +# Version 1.3.0 +# +# Copyright (c) 2003 - 2005 Austin Ziegler +# +# $Id: simple.rb,v 1.5 2005/05/05 16:16:49 austin Exp $ +#++ + # The "Transaction" namespace can be used for additional transaction + # support objects and modules. +module Transaction + # A standard exception for transaction errors. + class TransactionError < StandardError; end + # The TransactionAborted exception is used to indicate when a + # transaction has been aborted in the block form. + class TransactionAborted < Exception; end + # The TransactionCommitted exception is used to indicate when a + # transaction has been committed in the block form. + class TransactionCommitted < Exception; end + + te = "Transaction Error: %s" + + Messages = { + :bad_debug_object => + te % "the transaction debug object must respond to #<<.", + :unique_names => + te % "named transactions must be unique.", + :no_transaction_open => + te % "no transaction open.", + :cannot_rewind_no_transaction => + te % "cannot rewind; there is no current transaction.", + :cannot_rewind_named_transaction => + te % "cannot rewind to transaction %s because it does not exist.", + :cannot_rewind_transaction_before_block => + te % "cannot rewind a transaction started before the execution block.", + :cannot_abort_no_transaction => + te % "cannot abort; there is no current transaction.", + :cannot_abort_transaction_before_block => + te % "cannot abort a transaction started before the execution block.", + :cannot_abort_named_transaction => + te % "cannot abort nonexistant transaction %s.", + :cannot_commit_no_transaction => + te % "cannot commit; there is no current transaction.", + :cannot_commit_transaction_before_block => + te % "cannot commit a transaction started before the execution block.", + :cannot_commit_named_transaction => + te % "cannot commit nonexistant transaction %s.", + :cannot_start_empty_block_transaction => + te % "cannot start a block transaction with no objects.", + :cannot_obtain_transaction_lock => + te % "cannot obtain transaction lock for #%s.", + } + + # = Transaction::Simple for Ruby + # Simple object transaction support for Ruby + # + # == Introduction + # Transaction::Simple provides a generic way to add active transaction + # support to objects. The transaction methods added by this module will + # work with most objects, excluding those that cannot be + # Marshaled (bindings, procedure objects, IO instances, or + # singleton objects). + # + # The transactions supported by Transaction::Simple are not backed + # transactions; they are not associated with any sort of data store. + # They are "live" transactions occurring in memory and in the object + # itself. This is to allow "test" changes to be made to an object + # before making the changes permanent. + # + # Transaction::Simple can handle an "infinite" number of transaction + # levels (limited only by memory). If I open two transactions, commit + # the second, but abort the first, the object will revert to the + # original version. + # + # Transaction::Simple supports "named" transactions, so that multiple + # levels of transactions can be committed, aborted, or rewound by + # referring to the appropriate name of the transaction. Names may be any + # object *except* +nil+. As with Hash keys, String names will be + # duplicated and frozen before using. + # + # Copyright:: Copyright © 2003 - 2005 by Austin Ziegler + # Version:: 1.3.0 + # Licence:: MIT-Style + # + # Thanks to David Black for help with the initial concept that led to + # this library. + # + # == Usage + # include 'transaction/simple' + # + # v = "Hello, you." # -> "Hello, you." + # v.extend(Transaction::Simple) # -> "Hello, you." + # + # v.start_transaction # -> ... (a Marshal string) + # v.transaction_open? # -> true + # v.gsub!(/you/, "world") # -> "Hello, world." + # + # v.rewind_transaction # -> "Hello, you." + # v.transaction_open? # -> true + # + # v.gsub!(/you/, "HAL") # -> "Hello, HAL." + # v.abort_transaction # -> "Hello, you." + # v.transaction_open? # -> false + # + # v.start_transaction # -> ... (a Marshal string) + # v.start_transaction # -> ... (a Marshal string) + # + # v.transaction_open? # -> true + # v.gsub!(/you/, "HAL") # -> "Hello, HAL." + # + # v.commit_transaction # -> "Hello, HAL." + # v.transaction_open? # -> true + # v.abort_transaction # -> "Hello, you." + # v.transaction_open? # -> false + # + # == Named Transaction Usage + # v = "Hello, you." # -> "Hello, you." + # v.extend(Transaction::Simple) # -> "Hello, you." + # + # v.start_transaction(:first) # -> ... (a Marshal string) + # v.transaction_open? # -> true + # v.transaction_open?(:first) # -> true + # v.transaction_open?(:second) # -> false + # v.gsub!(/you/, "world") # -> "Hello, world." + # + # v.start_transaction(:second) # -> ... (a Marshal string) + # v.gsub!(/world/, "HAL") # -> "Hello, HAL." + # v.rewind_transaction(:first) # -> "Hello, you." + # v.transaction_open? # -> true + # v.transaction_open?(:first) # -> true + # v.transaction_open?(:second) # -> false + # + # v.gsub!(/you/, "world") # -> "Hello, world." + # v.start_transaction(:second) # -> ... (a Marshal string) + # v.gsub!(/world/, "HAL") # -> "Hello, HAL." + # v.transaction_name # -> :second + # v.abort_transaction(:first) # -> "Hello, you." + # v.transaction_open? # -> false + # + # v.start_transaction(:first) # -> ... (a Marshal string) + # v.gsub!(/you/, "world") # -> "Hello, world." + # v.start_transaction(:second) # -> ... (a Marshal string) + # v.gsub!(/world/, "HAL") # -> "Hello, HAL." + # + # v.commit_transaction(:first) # -> "Hello, HAL." + # v.transaction_open? # -> false + # + # == Block Usage + # v = "Hello, you." # -> "Hello, you." + # Transaction::Simple.start(v) do |tv| + # # v has been extended with Transaction::Simple and an unnamed + # # transaction has been started. + # tv.transaction_open? # -> true + # tv.gsub!(/you/, "world") # -> "Hello, world." + # + # tv.rewind_transaction # -> "Hello, you." + # tv.transaction_open? # -> true + # + # tv.gsub!(/you/, "HAL") # -> "Hello, HAL." + # # The following breaks out of the transaction block after + # # aborting the transaction. + # tv.abort_transaction # -> "Hello, you." + # end + # # v still has Transaction::Simple applied from here on out. + # v.transaction_open? # -> false + # + # Transaction::Simple.start(v) do |tv| + # tv.start_transaction # -> ... (a Marshal string) + # + # tv.transaction_open? # -> true + # tv.gsub!(/you/, "HAL") # -> "Hello, HAL." + # + # # If #commit_transaction were called without having started a + # # second transaction, then it would break out of the transaction + # # block after committing the transaction. + # tv.commit_transaction # -> "Hello, HAL." + # tv.transaction_open? # -> true + # tv.abort_transaction # -> "Hello, you." + # end + # v.transaction_open? # -> false + # + # == Named Transaction Usage + # v = "Hello, you." # -> "Hello, you." + # v.extend(Transaction::Simple) # -> "Hello, you." + # + # v.start_transaction(:first) # -> ... (a Marshal string) + # v.transaction_open? # -> true + # v.transaction_open?(:first) # -> true + # v.transaction_open?(:second) # -> false + # v.gsub!(/you/, "world") # -> "Hello, world." + # + # v.start_transaction(:second) # -> ... (a Marshal string) + # v.gsub!(/world/, "HAL") # -> "Hello, HAL." + # v.rewind_transaction(:first) # -> "Hello, you." + # v.transaction_open? # -> true + # v.transaction_open?(:first) # -> true + # v.transaction_open?(:second) # -> false + # + # v.gsub!(/you/, "world") # -> "Hello, world." + # v.start_transaction(:second) # -> ... (a Marshal string) + # v.gsub!(/world/, "HAL") # -> "Hello, HAL." + # v.transaction_name # -> :second + # v.abort_transaction(:first) # -> "Hello, you." + # v.transaction_open? # -> false + # + # v.start_transaction(:first) # -> ... (a Marshal string) + # v.gsub!(/you/, "world") # -> "Hello, world." + # v.start_transaction(:second) # -> ... (a Marshal string) + # v.gsub!(/world/, "HAL") # -> "Hello, HAL." + # + # v.commit_transaction(:first) # -> "Hello, HAL." + # v.transaction_open? # -> false + # + # == Thread Safety + # Threadsafe version of Transaction::Simple and + # Transaction::Simple::Group exist; these are loaded from + # 'transaction/simple/threadsafe' and + # 'transaction/simple/threadsafe/group', respectively, and are + # represented in Ruby code as Transaction::Simple::ThreadSafe and + # Transaction::Simple::ThreadSafe::Group, respectively. + # + # == Contraindications + # While Transaction::Simple is very useful, it has some severe + # limitations that must be understood. Transaction::Simple: + # + # * uses Marshal. Thus, any object which cannot be Marshaled + # cannot use Transaction::Simple. In my experience, this affects + # singleton objects more often than any other object. It may be that + # Ruby 2.0 will solve this problem. + # * does not manage resources. Resources external to the object and its + # instance variables are not managed at all. However, all instance + # variables and objects "belonging" to those instance variables are + # managed. If there are object reference counts to be handled, + # Transaction::Simple will probably cause problems. + # * is not inherently thread-safe. In the ACID ("atomic, consistent, + # isolated, durable") test, Transaction::Simple provides CD, but it is + # up to the user of Transaction::Simple to provide isolation and + # atomicity. Transactions should be considered "critical sections" in + # multi-threaded applications. If thread safety and atomicity is + # absolutely required, use Transaction::Simple::ThreadSafe, which uses + # a Mutex object to synchronize the accesses on the object during the + # transaction operations. + # * does not necessarily maintain Object#__id__ values on rewind or + # abort. This may change for future versions that will be Ruby 1.8 or + # better *only*. Certain objects that support #replace will maintain + # Object#__id__. + # * Can be a memory hog if you use many levels of transactions on many + # objects. + # + module Simple + TRANSACTION_SIMPLE_VERSION = '1.3.0' + + # Sets the Transaction::Simple debug object. It must respond to #<<. + # Sets the transaction debug object. Debugging will be performed + # automatically if there's a debug object. The generic transaction + # error class. + def self.debug_io=(io) + if io.nil? + @tdi = nil + @debugging = false + else + unless io.respond_to?(:<<) + raise TransactionError, Messages[:bad_debug_object] + end + @tdi = io + @debugging = true + end + end + + # Returns +true+ if we are debugging. + def self.debugging? + @debugging + end + + # Returns the Transaction::Simple debug object. It must respond to + # #<<. + def self.debug_io + @tdi ||= "" + @tdi + end + + # If +name+ is +nil+ (default), then returns +true+ if there is + # currently a transaction open. + # + # If +name+ is specified, then returns +true+ if there is currently a + # transaction that responds to +name+ open. + def transaction_open?(name = nil) + if name.nil? + if Transaction::Simple.debugging? + Transaction::Simple.debug_io << "Transaction " << + "[#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" + end + return (not @__transaction_checkpoint__.nil?) + else + if Transaction::Simple.debugging? + Transaction::Simple.debug_io << "Transaction(#{name.inspect}) " << + "[#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" + end + return ((not @__transaction_checkpoint__.nil?) and @__transaction_names__.include?(name)) + end + end + + # Returns the current name of the transaction. Transactions not + # explicitly named are named +nil+. + def transaction_name + if @__transaction_checkpoint__.nil? + raise TransactionError, Messages[:no_transaction_open] + end + if Transaction::Simple.debugging? + Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} " << + "Transaction Name: #{@__transaction_names__[-1].inspect}\n" + end + if @__transaction_names__[-1].kind_of?(String) + @__transaction_names__[-1].dup + else + @__transaction_names__[-1] + end + end + + # Starts a transaction. Stores the current object state. If a + # transaction name is specified, the transaction will be named. + # Transaction names must be unique. Transaction names of +nil+ will be + # treated as unnamed transactions. + def start_transaction(name = nil) + @__transaction_level__ ||= 0 + @__transaction_names__ ||= [] + + if name.nil? + @__transaction_names__ << nil + ss = "" if Transaction::Simple.debugging? + else + if @__transaction_names__.include?(name) + raise TransactionError, Messages[:unique_names] + end + name = name.dup.freeze if name.kind_of?(String) + @__transaction_names__ << name + ss = "(#{name.inspect})" if Transaction::Simple.debugging? + end + + @__transaction_level__ += 1 + + if Transaction::Simple.debugging? + Transaction::Simple.debug_io << "#{'>' * @__transaction_level__} " << + "Start Transaction#{ss}\n" + end + + @__transaction_checkpoint__ = Marshal.dump(self) + end + + # Rewinds the transaction. If +name+ is specified, then the + # intervening transactions will be aborted and the named transaction + # will be rewound. Otherwise, only the current transaction is rewound. + def rewind_transaction(name = nil) + if @__transaction_checkpoint__.nil? + raise TransactionError, Messages[:cannot_rewind_no_transaction] + end + + # Check to see if we are trying to rewind a transaction that is + # outside of the current transaction block. + if @__transaction_block__ and name + nix = @__transaction_names__.index(name) + 1 + if nix < @__transaction_block__ + raise TransactionError, Messages[:cannot_rewind_transaction_before_block] + end + end + + if name.nil? + __rewind_this_transaction + ss = "" if Transaction::Simple.debugging? + else + unless @__transaction_names__.include?(name) + raise TransactionError, Messages[:cannot_rewind_named_transaction] % name.inspect + end + ss = "(#{name})" if Transaction::Simple.debugging? + + while @__transaction_names__[-1] != name + @__transaction_checkpoint__ = __rewind_this_transaction + if Transaction::Simple.debugging? + Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} " << + "Rewind Transaction#{ss}\n" + end + @__transaction_level__ -= 1 + @__transaction_names__.pop + end + __rewind_this_transaction + end + if Transaction::Simple.debugging? + Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} " << + "Rewind Transaction#{ss}\n" + end + self + end + + # Aborts the transaction. Resets the object state to what it was + # before the transaction was started and closes the transaction. If + # +name+ is specified, then the intervening transactions and the named + # transaction will be aborted. Otherwise, only the current transaction + # is aborted. + # + # If the current or named transaction has been started by a block + # (Transaction::Simple.start), then the execution of the block will be + # halted with +break+ +self+. + def abort_transaction(name = nil) + if @__transaction_checkpoint__.nil? + raise TransactionError, Messages[:cannot_abort_no_transaction] + end + + # Check to see if we are trying to abort a transaction that is + # outside of the current transaction block. Otherwise, raise + # TransactionAborted if they are the same. + if @__transaction_block__ and name + nix = @__transaction_names__.index(name) + 1 + if nix < @__transaction_block__ + raise TransactionError, Messages[:cannot_abort_transaction_before_block] + end + + raise TransactionAborted if @__transaction_block__ == nix + end + + raise TransactionAborted if @__transaction_block__ == @__transaction_level__ + + if name.nil? + __abort_transaction(name) + else + unless @__transaction_names__.include?(name) + raise TransactionError, Messages[:cannot_abort_named_transaction] % name.inspect + end + __abort_transaction(name) while @__transaction_names__.include?(name) + end + self + end + + # If +name+ is +nil+ (default), the current transaction level is + # closed out and the changes are committed. + # + # If +name+ is specified and +name+ is in the list of named + # transactions, then all transactions are closed and committed until + # the named transaction is reached. + def commit_transaction(name = nil) + if @__transaction_checkpoint__.nil? + raise TransactionError, Messages[:cannot_commit_no_transaction] + end + @__transaction_block__ ||= nil + + # Check to see if we are trying to commit a transaction that is + # outside of the current transaction block. Otherwise, raise + # TransactionCommitted if they are the same. + if @__transaction_block__ and name + nix = @__transaction_names__.index(name) + 1 + if nix < @__transaction_block__ + raise TransactionError, Messages[:cannot_commit_transaction_before_block] + end + + raise TransactionCommitted if @__transaction_block__ == nix + end + + raise TransactionCommitted if @__transaction_block__ == @__transaction_level__ + + if name.nil? + ss = "" if Transaction::Simple.debugging? + __commit_transaction + if Transaction::Simple.debugging? + Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " << + "Commit Transaction#{ss}\n" + end + else + unless @__transaction_names__.include?(name) + raise TransactionError, Messages[:cannot_commit_named_transaction] % name.inspect + end + ss = "(#{name})" if Transaction::Simple.debugging? + + while @__transaction_names__[-1] != name + if Transaction::Simple.debugging? + Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " << + "Commit Transaction#{ss}\n" + end + __commit_transaction + end + if Transaction::Simple.debugging? + Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " << + "Commit Transaction#{ss}\n" + end + __commit_transaction + end + + self + end + + # Alternative method for calling the transaction methods. An optional + # name can be specified for named transaction support. + # + # #transaction(:start):: #start_transaction + # #transaction(:rewind):: #rewind_transaction + # #transaction(:abort):: #abort_transaction + # #transaction(:commit):: #commit_transaction + # #transaction(:name):: #transaction_name + # #transaction:: #transaction_open? + def transaction(action = nil, name = nil) + case action + when :start + start_transaction(name) + when :rewind + rewind_transaction(name) + when :abort + abort_transaction(name) + when :commit + commit_transaction(name) + when :name + transaction_name + when nil + transaction_open?(name) + end + end + + # Allows specific variables to be excluded from transaction support. + # Must be done after extending the object but before starting the + # first transaction on the object. + # + # vv.transaction_exclusions << "@io" + def transaction_exclusions + @transaction_exclusions ||= [] + end + + class << self + def __common_start(name, vars, &block) + if vars.empty? + raise TransactionError, Messages[:cannot_start_empty_block_transaction] + end + + if block + begin + vlevel = {} + + vars.each do |vv| + vv.extend(Transaction::Simple) + vv.start_transaction(name) + vlevel[vv.__id__] = vv.instance_variable_get(:@__transaction_level__) + vv.instance_variable_set(:@__transaction_block__, vlevel[vv.__id__]) + end + + yield(*vars) + rescue TransactionAborted + vars.each do |vv| + if name.nil? and vv.transaction_open? + loop do + tlevel = vv.instance_variable_get(:@__transaction_level__) || -1 + vv.instance_variable_set(:@__transaction_block__, -1) + break if tlevel < vlevel[vv.__id__] + vv.abort_transaction if vv.transaction_open? + end + elsif vv.transaction_open?(name) + vv.instance_variable_set(:@__transaction_block__, -1) + vv.abort_transaction(name) + end + end + rescue TransactionCommitted + nil + ensure + vars.each do |vv| + if name.nil? and vv.transaction_open? + loop do + tlevel = vv.instance_variable_get(:@__transaction_level__) || -1 + break if tlevel < vlevel[vv.__id__] + vv.instance_variable_set(:@__transaction_block__, -1) + vv.commit_transaction if vv.transaction_open? + end + elsif vv.transaction_open?(name) + vv.instance_variable_set(:@__transaction_block__, -1) + vv.commit_transaction(name) + end + end + end + else + vars.each do |vv| + vv.extend(Transaction::Simple) + vv.start_transaction(name) + end + end + end + private :__common_start + + def start_named(name, *vars, &block) + __common_start(name, vars, &block) + end + + def start(*vars, &block) + __common_start(nil, vars, &block) + end + end + + def __abort_transaction(name = nil) #:nodoc: + @__transaction_checkpoint__ = __rewind_this_transaction + + if name.nil? + ss = "" if Transaction::Simple.debugging? + else + ss = "(#{name.inspect})" if Transaction::Simple.debugging? + end + + if Transaction::Simple.debugging? + Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " << + "Abort Transaction#{ss}\n" + end + @__transaction_level__ -= 1 + @__transaction_names__.pop + if @__transaction_level__ < 1 + @__transaction_level__ = 0 + @__transaction_names__ = [] + end + end + + TRANSACTION_CHECKPOINT = "@__transaction_checkpoint__" #:nodoc: + SKIP_TRANSACTION_VARS = [TRANSACTION_CHECKPOINT, "@__transaction_level__"] #:nodoc: + + def __rewind_this_transaction #:nodoc: + rr = Marshal.restore(@__transaction_checkpoint__) + + begin + self.replace(rr) if respond_to?(:replace) + rescue + nil + end + + rr.instance_variables.each do |vv| + next if SKIP_TRANSACTION_VARS.include?(vv) + next if self.transaction_exclusions.include?(vv) + if respond_to?(:instance_variable_get) + instance_variable_set(vv, rr.instance_variable_get(vv)) + else + instance_eval(%q|#{vv} = rr.instance_eval("#{vv}")|) + end + end + + new_ivar = instance_variables - rr.instance_variables - SKIP_TRANSACTION_VARS + new_ivar.each do |vv| + if respond_to?(:instance_variable_set) + instance_variable_set(vv, nil) + else + instance_eval(%q|#{vv} = nil|) + end + end + + if respond_to?(:instance_variable_get) + rr.instance_variable_get(TRANSACTION_CHECKPOINT) + else + rr.instance_eval(TRANSACTION_CHECKPOINT) + end + end + + def __commit_transaction #:nodoc: + if respond_to?(:instance_variable_get) + @__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_variable_get(TRANSACTION_CHECKPOINT) + else + @__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_eval(TRANSACTION_CHECKPOINT) + end + + @__transaction_level__ -= 1 + @__transaction_names__.pop + + if @__transaction_level__ < 1 + @__transaction_level__ = 0 + @__transaction_names__ = [] + end + end + + private :__abort_transaction + private :__rewind_this_transaction + private :__commit_transaction + end +end diff --git a/vendor/rails/activerecord/lib/active_record/version.rb b/vendor/rails/activerecord/lib/active_record/version.rb new file mode 100644 index 00000000..4568a1b4 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/version.rb @@ -0,0 +1,9 @@ +module ActiveRecord + module VERSION #:nodoc: + MAJOR = 1 + MINOR = 14 + TINY = 4 + + STRING = [MAJOR, MINOR, TINY].join('.') + end +end diff --git a/vendor/rails/activerecord/lib/active_record/wrappers/yaml_wrapper.rb b/vendor/rails/activerecord/lib/active_record/wrappers/yaml_wrapper.rb new file mode 100644 index 00000000..74f40a50 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/wrappers/yaml_wrapper.rb @@ -0,0 +1,15 @@ +require 'yaml' + +module ActiveRecord + module Wrappings #:nodoc: + class YamlWrapper < AbstractWrapper #:nodoc: + def wrap(attribute) attribute.to_yaml end + def unwrap(attribute) YAML::load(attribute) end + end + + module ClassMethods #:nodoc: + # Wraps the attribute in Yaml encoding + def wrap_in_yaml(*attributes) wrap_with(YamlWrapper, attributes) end + end + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/lib/active_record/wrappings.rb b/vendor/rails/activerecord/lib/active_record/wrappings.rb new file mode 100644 index 00000000..01976417 --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/wrappings.rb @@ -0,0 +1,59 @@ +module ActiveRecord + # A plugin framework for wrapping attribute values before they go in and unwrapping them after they go out of the database. + # This was intended primarily for YAML wrapping of arrays and hashes, but this behavior is now native in the Base class. + # So for now this framework is laying dormant until a need pops up. + module Wrappings #:nodoc: + module ClassMethods #:nodoc: + def wrap_with(wrapper, *attributes) + [ attributes ].flat.each { |attribute| wrapper.wrap(attribute) } + end + end + + def self.append_features(base) + super + base.extend(ClassMethods) + end + + class AbstractWrapper #:nodoc: + def self.wrap(attribute, record_binding) #:nodoc: + %w( before_save after_save after_initialize ).each do |callback| + eval "#{callback} #{name}.new('#{attribute}')", record_binding + end + end + + def initialize(attribute) #:nodoc: + @attribute = attribute + end + + def save_wrapped_attribute(record) #:nodoc: + if record.attribute_present?(@attribute) + record.send( + "write_attribute", + @attribute, + wrap(record.send("read_attribute", @attribute)) + ) + end + end + + def load_wrapped_attribute(record) #:nodoc: + if record.attribute_present?(@attribute) + record.send( + "write_attribute", + @attribute, + unwrap(record.send("read_attribute", @attribute)) + ) + end + end + + alias_method :before_save, :save_wrapped_attribute #:nodoc: + alias_method :after_save, :load_wrapped_attribute #:nodoc: + alias_method :after_initialize, :after_save #:nodoc: + + # Overwrite to implement the logic that'll take the regular attribute and wrap it. + def wrap(attribute) end + + # Overwrite to implement the logic that'll take the wrapped attribute and unwrap it. + def unwrap(attribute) end + end + end +end diff --git a/vendor/rails/activerecord/test/aaa_create_tables_test.rb b/vendor/rails/activerecord/test/aaa_create_tables_test.rb new file mode 100644 index 00000000..8ff6c64e --- /dev/null +++ b/vendor/rails/activerecord/test/aaa_create_tables_test.rb @@ -0,0 +1,55 @@ +# The filename begins with "aaa" to ensure this is the first test. +require 'abstract_unit' + +class AAACreateTablesTest < Test::Unit::TestCase + self.use_transactional_fixtures = false + + def setup + @base_path = "#{File.dirname(__FILE__)}/fixtures/db_definitions" + end + + def test_drop_and_create_main_tables + recreate ActiveRecord::Base + assert true + end + + def test_load_schema + eval(File.read("#{File.dirname(__FILE__)}/fixtures/db_definitions/schema.rb")) + assert true + end + + def test_drop_and_create_courses_table + recreate Course, '2' + assert true + end + + private + def recreate(base, suffix = nil) + connection = base.connection + adapter_name = connection.adapter_name.downcase + suffix.to_s + execute_sql_file "#{@base_path}/#{adapter_name}.drop.sql", connection + execute_sql_file "#{@base_path}/#{adapter_name}.sql", connection + end + + def execute_sql_file(path, connection) + # OpenBase has a different format for sql files + if current_adapter?(:OpenBaseAdapter) then + File.read(path).split("go").each_with_index do |sql, i| + begin + # OpenBase does not support comments embedded in sql + connection.execute(sql,"SQL statement ##{i}") unless sql.blank? + rescue ActiveRecord::StatementInvalid + #$stderr.puts "warning: #{$!}" + end + end + else + File.read(path).split(';').each_with_index do |sql, i| + begin + connection.execute("\n\n-- statement ##{i}\n#{sql}\n") unless sql.blank? + rescue ActiveRecord::StatementInvalid + #$stderr.puts "warning: #{$!}" + end + end + end + end +end diff --git a/vendor/rails/activerecord/test/abstract_unit.rb b/vendor/rails/activerecord/test/abstract_unit.rb new file mode 100755 index 00000000..29e4601f --- /dev/null +++ b/vendor/rails/activerecord/test/abstract_unit.rb @@ -0,0 +1,67 @@ +$:.unshift(File.dirname(__FILE__) + '/../lib') +$:.unshift(File.dirname(__FILE__) + '/../../activesupport/lib') + +require 'test/unit' +require 'active_record' +require 'active_record/fixtures' +require 'active_support/binding_of_caller' +require 'active_support/breakpoint' +require 'connection' + +QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name('type') unless Object.const_defined?(:QUOTED_TYPE) + +class Test::Unit::TestCase #:nodoc: + self.fixture_path = File.dirname(__FILE__) + "/fixtures/" + self.use_instantiated_fixtures = false + self.use_transactional_fixtures = (ENV['AR_NO_TX_FIXTURES'] != "yes") + + def create_fixtures(*table_names, &block) + Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures/", table_names, {}, &block) + end + + def assert_date_from_db(expected, actual, message = nil) + # SQL Server doesn't have a separate column type just for dates, + # so the time is in the string and incorrectly formatted + if current_adapter?(:SQLServerAdapter) + assert_equal expected.strftime("%Y/%m/%d 00:00:00"), actual.strftime("%Y/%m/%d 00:00:00") + elsif current_adapter?(:SybaseAdapter) + assert_equal expected.to_s, actual.to_date.to_s, message + else + assert_equal expected.to_s, actual.to_s, message + end + end + + def assert_queries(num = 1) + ActiveRecord::Base.connection.class.class_eval do + self.query_count = 0 + alias_method :execute, :execute_with_query_counting + end + yield + ensure + ActiveRecord::Base.connection.class.class_eval do + alias_method :execute, :execute_without_query_counting + end + assert_equal num, ActiveRecord::Base.connection.query_count, "#{ActiveRecord::Base.connection.query_count} instead of #{num} queries were executed." + end + + def assert_no_queries(&block) + assert_queries(0, &block) + end +end + +def current_adapter?(type) + ActiveRecord::ConnectionAdapters.const_defined?(type) && + ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters.const_get(type)) +end + +ActiveRecord::Base.connection.class.class_eval do + cattr_accessor :query_count + alias_method :execute_without_query_counting, :execute + def execute_with_query_counting(sql, name = nil) + self.query_count += 1 + execute_without_query_counting(sql, name) + end +end + +#ActiveRecord::Base.logger = Logger.new(STDOUT) +#ActiveRecord::Base.colorize_logging = false diff --git a/vendor/rails/activerecord/test/active_schema_mysql.rb b/vendor/rails/activerecord/test/active_schema_mysql.rb new file mode 100644 index 00000000..d3ee5bfb --- /dev/null +++ b/vendor/rails/activerecord/test/active_schema_mysql.rb @@ -0,0 +1,31 @@ +require 'abstract_unit' + +class ActiveSchemaTest < Test::Unit::TestCase + def setup + ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do + alias_method :real_execute, :execute + def execute(sql, name = nil) return sql end + end + end + + def teardown + ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:alias_method, :execute, :real_execute) + end + + def test_drop_table + assert_equal "DROP TABLE people", drop_table(:people) + end + + def test_add_column + assert_equal "ALTER TABLE people ADD last_name varchar(255)", add_column(:people, :last_name, :string) + end + + def test_add_column_with_limit + assert_equal "ALTER TABLE people ADD key varchar(32)", add_column(:people, :key, :string, :limit => 32) + end + + private + def method_missing(method_symbol, *arguments) + ActiveRecord::Base.connection.send(method_symbol, *arguments) + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/adapter_test.rb b/vendor/rails/activerecord/test/adapter_test.rb new file mode 100644 index 00000000..c4027b31 --- /dev/null +++ b/vendor/rails/activerecord/test/adapter_test.rb @@ -0,0 +1,85 @@ +require 'abstract_unit' + +class AdapterTest < Test::Unit::TestCase + def setup + @connection = ActiveRecord::Base.connection + end + + def test_tables + if @connection.respond_to?(:tables) + tables = @connection.tables + assert tables.include?("accounts") + assert tables.include?("authors") + assert tables.include?("tasks") + assert tables.include?("topics") + else + warn "#{@connection.class} does not respond to #tables" + end + end + + def test_indexes + idx_name = "accounts_idx" + + if @connection.respond_to?(:indexes) + indexes = @connection.indexes("accounts") + assert indexes.empty? + + @connection.add_index :accounts, :firm_id, :name => idx_name + indexes = @connection.indexes("accounts") + assert_equal "accounts", indexes.first.table + # OpenBase does not have the concept of a named index + # Indexes are merely properties of columns. + assert_equal idx_name, indexes.first.name unless current_adapter?(:OpenBaseAdapter) + assert !indexes.first.unique + assert_equal ["firm_id"], indexes.first.columns + else + warn "#{@connection.class} does not respond to #indexes" + end + + ensure + @connection.remove_index(:accounts, :name => idx_name) rescue nil + end + + def test_current_database + if @connection.respond_to?(:current_database) + assert_equal ENV['ARUNIT_DB_NAME'] || "activerecord_unittest", @connection.current_database + end + end + + def test_table_alias + def @connection.test_table_alias_length() 10; end + class << @connection + alias_method :old_table_alias_length, :table_alias_length + alias_method :table_alias_length, :test_table_alias_length + end + + assert_equal 'posts', @connection.table_alias_for('posts') + assert_equal 'posts_comm', @connection.table_alias_for('posts_comments') + assert_equal 'dbo_posts', @connection.table_alias_for('dbo.posts') + + class << @connection + alias_method :table_alias_length, :old_table_alias_length + end + end + + # test resetting sequences in odd tables in postgreSQL + if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!) + require 'fixtures/movie' + require 'fixtures/subscriber' + def test_reset_empty_table_with_custom_pk + Movie.delete_all + Movie.connection.reset_pk_sequence! 'movies' + assert_equal 1, Movie.create(:name => 'fight club').id + end + + def test_reset_table_with_non_integer_pk + Subscriber.delete_all + Subscriber.connection.reset_pk_sequence! 'subscribers' + + sub = Subscriber.new(:name => 'robert drake') + sub.id = 'bob drake' + assert_nothing_raised { sub.save! } + end + end + +end diff --git a/vendor/rails/activerecord/test/aggregations_test.rb b/vendor/rails/activerecord/test/aggregations_test.rb new file mode 100644 index 00000000..39adedae --- /dev/null +++ b/vendor/rails/activerecord/test/aggregations_test.rb @@ -0,0 +1,66 @@ +require 'abstract_unit' +require 'fixtures/customer' + +class AggregationsTest < Test::Unit::TestCase + fixtures :customers + + def test_find_single_value_object + assert_equal 50, customers(:david).balance.amount + assert_kind_of Money, customers(:david).balance + assert_equal 300, customers(:david).balance.exchange_to("DKK").amount + end + + def test_find_multiple_value_object + assert_equal customers(:david).address_street, customers(:david).address.street + assert( + customers(:david).address.close_to?(Address.new("Different Street", customers(:david).address_city, customers(:david).address_country)) + ) + end + + def test_change_single_value_object + customers(:david).balance = Money.new(100) + customers(:david).save + assert_equal 100, Customer.find(1).balance.amount + end + + def test_immutable_value_objects + customers(:david).balance = Money.new(100) + assert_raises(TypeError) { customers(:david).balance.instance_eval { @amount = 20 } } + end + + def test_inferred_mapping + assert_equal "35.544623640962634", customers(:david).gps_location.latitude + assert_equal "-105.9309951055148", customers(:david).gps_location.longitude + + customers(:david).gps_location = GpsLocation.new("39x-110") + + assert_equal "39", customers(:david).gps_location.latitude + assert_equal "-110", customers(:david).gps_location.longitude + + customers(:david).save + + customers(:david).reload + + assert_equal "39", customers(:david).gps_location.latitude + assert_equal "-110", customers(:david).gps_location.longitude + end + + def test_reloaded_instance_refreshes_aggregations + assert_equal "35.544623640962634", customers(:david).gps_location.latitude + assert_equal "-105.9309951055148", customers(:david).gps_location.longitude + + Customer.update_all("gps_location = '24x113'") + customers(:david).reload + assert_equal '24x113', customers(:david)['gps_location'] + + assert_equal GpsLocation.new('24x113'), customers(:david).gps_location + end + + def test_gps_equality + assert GpsLocation.new('39x110') == GpsLocation.new('39x110') + end + + def test_gps_inequality + assert GpsLocation.new('39x110') != GpsLocation.new('39x111') + end +end diff --git a/vendor/rails/activerecord/test/all.sh b/vendor/rails/activerecord/test/all.sh new file mode 100755 index 00000000..a6712cc4 --- /dev/null +++ b/vendor/rails/activerecord/test/all.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +if [ -z "$1" ]; then + echo "Usage: $0 connections/" 1>&2 + exit 1 +fi + +ruby -I $1 -e 'Dir.foreach(".") { |file| require file if file =~ /_test.rb$/ }' diff --git a/vendor/rails/activerecord/test/ar_schema_test.rb b/vendor/rails/activerecord/test/ar_schema_test.rb new file mode 100644 index 00000000..c700b85e --- /dev/null +++ b/vendor/rails/activerecord/test/ar_schema_test.rb @@ -0,0 +1,33 @@ +require 'abstract_unit' +require "#{File.dirname(__FILE__)}/../lib/active_record/schema" + +if ActiveRecord::Base.connection.supports_migrations? + + class ActiveRecordSchemaTest < Test::Unit::TestCase + self.use_transactional_fixtures = false + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + @connection.drop_table :fruits rescue nil + end + + def test_schema_define + ActiveRecord::Schema.define(:version => 7) do + create_table :fruits do |t| + t.column :color, :string + t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle + t.column :texture, :string + t.column :flavor, :string + end + end + + assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } + assert_nothing_raised { @connection.select_all "SELECT * FROM schema_info" } + assert_equal 7, @connection.select_one("SELECT version FROM schema_info")['version'].to_i + end + end + +end diff --git a/vendor/rails/activerecord/test/association_callbacks_test.rb b/vendor/rails/activerecord/test/association_callbacks_test.rb new file mode 100644 index 00000000..1426fb71 --- /dev/null +++ b/vendor/rails/activerecord/test/association_callbacks_test.rb @@ -0,0 +1,124 @@ +require 'abstract_unit' +require 'fixtures/post' +require 'fixtures/comment' +require 'fixtures/author' +require 'fixtures/category' +require 'fixtures/project' +require 'fixtures/developer' + +class AssociationCallbacksTest < Test::Unit::TestCase + fixtures :posts, :authors, :projects, :developers + + def setup + @david = authors(:david) + @thinking = posts(:thinking) + @authorless = posts(:authorless) + assert @david.post_log.empty? + end + + def test_adding_macro_callbacks + @david.posts_with_callbacks << @thinking + assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}"], @david.post_log + @david.posts_with_callbacks << @thinking + assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}", "before_adding#{@thinking.id}", + "after_adding#{@thinking.id}"], @david.post_log + end + + def test_adding_with_proc_callbacks + @david.posts_with_proc_callbacks << @thinking + assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}"], @david.post_log + @david.posts_with_proc_callbacks << @thinking + assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}", "before_adding#{@thinking.id}", + "after_adding#{@thinking.id}"], @david.post_log + end + + def test_removing_with_macro_callbacks + first_post, second_post = @david.posts_with_callbacks[0, 2] + @david.posts_with_callbacks.delete(first_post) + assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}"], @david.post_log + @david.posts_with_callbacks.delete(second_post) + assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}", "before_removing#{second_post.id}", + "after_removing#{second_post.id}"], @david.post_log + end + + def test_removing_with_proc_callbacks + first_post, second_post = @david.posts_with_callbacks[0, 2] + @david.posts_with_proc_callbacks.delete(first_post) + assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}"], @david.post_log + @david.posts_with_proc_callbacks.delete(second_post) + assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}", "before_removing#{second_post.id}", + "after_removing#{second_post.id}"], @david.post_log + end + + def test_multiple_callbacks + @david.posts_with_multiple_callbacks << @thinking + assert_equal ["before_adding#{@thinking.id}", "before_adding_proc#{@thinking.id}", "after_adding#{@thinking.id}", + "after_adding_proc#{@thinking.id}"], @david.post_log + @david.posts_with_multiple_callbacks << @thinking + assert_equal ["before_adding#{@thinking.id}", "before_adding_proc#{@thinking.id}", "after_adding#{@thinking.id}", + "after_adding_proc#{@thinking.id}", "before_adding#{@thinking.id}", "before_adding_proc#{@thinking.id}", + "after_adding#{@thinking.id}", "after_adding_proc#{@thinking.id}"], @david.post_log + end + + def test_has_and_belongs_to_many_add_callback + david = developers(:david) + ar = projects(:active_record) + assert ar.developers_log.empty? + ar.developers_with_callbacks << david + assert_equal ["before_adding#{david.id}", "after_adding#{david.id}"], ar.developers_log + ar.developers_with_callbacks << david + assert_equal ["before_adding#{david.id}", "after_adding#{david.id}", "before_adding#{david.id}", + "after_adding#{david.id}"], ar.developers_log + end + + def test_has_and_belongs_to_many_remove_callback + david = developers(:david) + jamis = developers(:jamis) + activerecord = projects(:active_record) + assert activerecord.developers_log.empty? + activerecord.developers_with_callbacks.delete(david) + assert_equal ["before_removing#{david.id}", "after_removing#{david.id}"], activerecord.developers_log + + activerecord.developers_with_callbacks.delete(jamis) + assert_equal ["before_removing#{david.id}", "after_removing#{david.id}", "before_removing#{jamis.id}", + "after_removing#{jamis.id}"], activerecord.developers_log + end + + def test_has_and_belongs_to_many_remove_callback_on_clear + activerecord = projects(:active_record) + assert activerecord.developers_log.empty? + if activerecord.developers_with_callbacks.size == 0 + activerecord.developers << developers(:david) + activerecord.developers << developers(:jamis) + activerecord.reload + assert activerecord.developers_with_callbacks.size == 2 + end + log_array = activerecord.developers_with_callbacks.collect {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.flatten.sort + assert activerecord.developers_with_callbacks.clear + assert_equal log_array, activerecord.developers_log.sort + end + + def test_dont_add_if_before_callback_raises_exception + assert !@david.unchangable_posts.include?(@authorless) + begin + @david.unchangable_posts << @authorless + rescue Exception => e + end + assert @david.post_log.empty? + assert !@david.unchangable_posts.include?(@authorless) + @david.reload + assert !@david.unchangable_posts.include?(@authorless) + end + + def test_push_with_attributes + david = developers(:david) + activerecord = projects(:active_record) + assert activerecord.developers_log.empty? + activerecord.developers_with_callbacks.push_with_attributes(david, {}) + assert_equal ["before_adding#{david.id}", "after_adding#{david.id}"], activerecord.developers_log + activerecord.developers_with_callbacks.push_with_attributes(david, {}) + assert_equal ["before_adding#{david.id}", "after_adding#{david.id}", "before_adding#{david.id}", + "after_adding#{david.id}"], activerecord.developers_log + end +end + diff --git a/vendor/rails/activerecord/test/association_inheritance_reload.rb b/vendor/rails/activerecord/test/association_inheritance_reload.rb new file mode 100644 index 00000000..a3d57228 --- /dev/null +++ b/vendor/rails/activerecord/test/association_inheritance_reload.rb @@ -0,0 +1,14 @@ +require 'abstract_unit' +require 'fixtures/company' + +class AssociationInheritanceReloadTest < Test::Unit::TestCase + fixtures :companies + + def test_set_attributes + assert_equal ["errors.add_on_empty('name', \"can't be empty\")"], Firm.read_inheritable_attribute("validate"), "Second run" + # ActiveRecord::Base.reset_column_information_and_inheritable_attributes_for_all_subclasses + remove_subclass_of(ActiveRecord::Base) + load 'fixtures/company.rb' + assert_equal ["errors.add_on_empty('name', \"can't be empty\")"], Firm.read_inheritable_attribute("validate"), "Second run" + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/associations_cascaded_eager_loading_test.rb b/vendor/rails/activerecord/test/associations_cascaded_eager_loading_test.rb new file mode 100644 index 00000000..dd12d529 --- /dev/null +++ b/vendor/rails/activerecord/test/associations_cascaded_eager_loading_test.rb @@ -0,0 +1,106 @@ +require 'abstract_unit' +require 'active_record/acts/list' +require 'fixtures/post' +require 'fixtures/comment' +require 'fixtures/author' +require 'fixtures/category' +require 'fixtures/categorization' +require 'fixtures/mixin' +require 'fixtures/company' +require 'fixtures/topic' +require 'fixtures/reply' + +class CascadedEagerLoadingTest < Test::Unit::TestCase + fixtures :authors, :mixins, :companies, :posts, :categorizations, :topics + + def test_eager_association_loading_with_cascaded_two_levels + authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id") + assert_equal 2, authors.size + assert_equal 5, authors[0].posts.size + assert_equal 1, authors[1].posts.size + assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} + end + + def test_eager_association_loading_with_cascaded_two_levels_and_one_level + authors = Author.find(:all, :include=>[{:posts=>:comments}, :categorizations], :order=>"authors.id") + assert_equal 2, authors.size + assert_equal 5, authors[0].posts.size + assert_equal 1, authors[1].posts.size + assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} + assert_equal 1, authors[0].categorizations.size + assert_equal 1, authors[1].categorizations.size + end + + def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations + authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id") + assert_equal 2, authors.size + assert_equal 5, authors[0].posts.size + assert_equal 1, authors[1].posts.size + assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} + end + + def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference + authors = Author.find(:all, :include=>{:posts=>[:comments, :author]}, :order=>"authors.id") + assert_equal 2, authors.size + assert_equal 5, authors[0].posts.size + assert_equal authors(:david).name, authors[0].name + assert_equal [authors(:david).name], authors[0].posts.collect{|post| post.author.name}.uniq + end + + def test_eager_association_loading_with_cascaded_two_levels_with_condition + authors = Author.find(:all, :include=>{:posts=>:comments}, :conditions=>"authors.id=1", :order=>"authors.id") + assert_equal 1, authors.size + assert_equal 5, authors[0].posts.size + end + + def test_eager_association_loading_with_acts_as_tree + roots = TreeMixin.find(:all, :include=>"children", :conditions=>"mixins.parent_id IS NULL", :order=>"mixins.id") + assert_equal [mixins(:tree_1), mixins(:tree2_1), mixins(:tree3_1)], roots + assert_no_queries do + assert_equal 2, roots[0].children.size + assert_equal 0, roots[1].children.size + assert_equal 0, roots[2].children.size + end + end + + def test_eager_association_loading_with_cascaded_three_levels_by_ping_pong + firms = Firm.find(:all, :include=>{:account=>{:firm=>:account}}, :order=>"companies.id") + assert_equal 2, firms.size + assert_equal firms.first.account, firms.first.account.firm.account + assert_equal companies(:first_firm).account, assert_no_queries { firms.first.account.firm.account } + assert_equal companies(:first_firm).account.firm.account, assert_no_queries { firms.first.account.firm.account } + end + + def test_eager_association_loading_with_has_many_sti + topics = Topic.find(:all, :include => :replies, :order => 'topics.id') + assert_equal [topics(:first), topics(:second)], topics + assert_no_queries do + assert_equal 1, topics[0].replies.size + assert_equal 0, topics[1].replies.size + end + end + + def test_eager_association_loading_with_belongs_to_sti + replies = Reply.find(:all, :include => :topic, :order => 'topics.id') + assert_equal [topics(:second)], replies + assert_equal topics(:first), assert_no_queries { replies.first.topic } + end + + def test_eager_association_loading_with_multiple_stis_and_order + author = Author.find(:first, :include => { :posts => [ :special_comments , :very_special_comment ] }, :order => 'authors.name, comments.body, very_special_comments_posts.body', :conditions => 'posts.id = 4') + assert_equal authors(:david), author + assert_no_queries do + author.posts.first.special_comments + author.posts.first.very_special_comment + end + end + + def test_eager_association_loading_of_stis_with_multiple_references + authors = Author.find(:all, :include => { :posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } } }, :order => 'comments.body, very_special_comments_posts.body', :conditions => 'posts.id = 4') + assert_equal [authors(:david)], authors + assert_no_queries do + authors.first.posts.first.special_comments.first.post.special_comments + authors.first.posts.first.special_comments.first.post.very_special_comment + end + end +end diff --git a/vendor/rails/activerecord/test/associations_extensions_test.rb b/vendor/rails/activerecord/test/associations_extensions_test.rb new file mode 100644 index 00000000..915056c5 --- /dev/null +++ b/vendor/rails/activerecord/test/associations_extensions_test.rb @@ -0,0 +1,37 @@ +require 'abstract_unit' +require 'fixtures/post' +require 'fixtures/comment' +require 'fixtures/project' +require 'fixtures/developer' + +class AssociationsExtensionsTest < Test::Unit::TestCase + fixtures :projects, :developers, :developers_projects, :comments, :posts + + def test_extension_on_has_many + assert_equal comments(:more_greetings), posts(:welcome).comments.find_most_recent + end + + def test_extension_on_habtm + assert_equal projects(:action_controller), developers(:david).projects.find_most_recent + end + + def test_named_extension_on_habtm + assert_equal projects(:action_controller), developers(:david).projects_extended_by_name.find_most_recent + end + + def test_marshalling_extensions + david = developers(:david) + assert_equal projects(:action_controller), david.projects.find_most_recent + + david = Marshal.load(Marshal.dump(david)) + assert_equal projects(:action_controller), david.projects.find_most_recent + end + + def test_marshalling_named_extensions + david = developers(:david) + assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent + + david = Marshal.load(Marshal.dump(david)) + assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/associations_go_eager_test.rb b/vendor/rails/activerecord/test/associations_go_eager_test.rb new file mode 100644 index 00000000..d2ae4e0d --- /dev/null +++ b/vendor/rails/activerecord/test/associations_go_eager_test.rb @@ -0,0 +1,359 @@ +require 'abstract_unit' +require 'fixtures/post' +require 'fixtures/comment' +require 'fixtures/author' +require 'fixtures/category' +require 'fixtures/company' +require 'fixtures/person' +require 'fixtures/reader' + +class EagerAssociationTest < Test::Unit::TestCase + fixtures :posts, :comments, :authors, :categories, :categories_posts, + :companies, :accounts, :tags, :people, :readers + + def test_loading_with_one_association + posts = Post.find(:all, :include => :comments) + post = posts.find { |p| p.id == 1 } + assert_equal 2, post.comments.size + assert post.comments.include?(comments(:greetings)) + + post = Post.find(:first, :include => :comments, :conditions => "posts.title = 'Welcome to the weblog'") + assert_equal 2, post.comments.size + assert post.comments.include?(comments(:greetings)) + end + + def test_loading_conditions_with_or + posts = authors(:david).posts.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'") + assert_nil posts.detect { |p| p.author_id != authors(:david).id }, + "expected to find only david's posts" + end + + def test_with_ordering + list = Post.find(:all, :include => :comments, :order => "posts.id DESC") + [:eager_other, :sti_habtm, :sti_post_and_comments, :sti_comments, + :authorless, :thinking, :welcome + ].each_with_index do |post, index| + assert_equal posts(post), list[index] + end + end + + def test_loading_with_multiple_associations + posts = Post.find(:all, :include => [ :comments, :author, :categories ], :order => "posts.id") + assert_equal 2, posts.first.comments.size + assert_equal 2, posts.first.categories.size + assert posts.first.comments.include?(comments(:greetings)) + end + + def test_loading_from_an_association + posts = authors(:david).posts.find(:all, :include => :comments, :order => "posts.id") + assert_equal 2, posts.first.comments.size + end + + def test_loading_with_no_associations + assert_nil Post.find(posts(:authorless).id, :include => :author).author + end + + def test_eager_association_loading_with_belongs_to + comments = Comment.find(:all, :include => :post) + assert_equal 10, comments.length + titles = comments.map { |c| c.post.title } + assert titles.include?(posts(:welcome).title) + assert titles.include?(posts(:sti_post_and_comments).title) + end + + def test_eager_association_loading_with_belongs_to_and_limit + comments = Comment.find(:all, :include => :post, :limit => 5, :order => 'comments.id') + assert_equal 5, comments.length + assert_equal [1,2,3,5,6], comments.collect { |c| c.id } + end + + def test_eager_association_loading_with_belongs_to_and_limit_and_conditions + comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :order => 'comments.id') + assert_equal 3, comments.length + assert_equal [5,6,7], comments.collect { |c| c.id } + end + + def test_eager_association_loading_with_belongs_to_and_limit_and_offset + comments = Comment.find(:all, :include => :post, :limit => 3, :offset => 2, :order => 'comments.id') + assert_equal 3, comments.length + assert_equal [3,5,6], comments.collect { |c| c.id } + end + + def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions + comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id') + assert_equal 3, comments.length + assert_equal [6,7,8], comments.collect { |c| c.id } + end + + def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array + comments = Comment.find(:all, :include => :post, :conditions => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id') + assert_equal 3, comments.length + assert_equal [6,7,8], comments.collect { |c| c.id } + end + + def test_eager_association_loading_with_belongs_to_and_limit_and_multiple_associations + posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :order => 'posts.id') + assert_equal 1, posts.length + assert_equal [1], posts.collect { |p| p.id } + end + + def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations + posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id') + assert_equal 1, posts.length + assert_equal [2], posts.collect { |p| p.id } + end + + def test_eager_with_has_many_through + posts_with_comments = people(:michael).posts.find(:all, :include => :comments ) + posts_with_author = people(:michael).posts.find(:all, :include => :author ) + posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ]) + assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size } + assert_equal authors(:david), assert_no_queries { posts_with_author.first.author } + assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author } + end + + def test_eager_with_has_many_and_limit + posts = Post.find(:all, :order => 'posts.id asc', :include => [ :author, :comments ], :limit => 2) + assert_equal 2, posts.size + assert_equal 3, posts.inject(0) { |sum, post| sum += post.comments.size } + end + + def test_eager_with_has_many_and_limit_and_conditions + posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.body = 'hello'", :order => "posts.id") + assert_equal 2, posts.size + assert_equal [4,5], posts.collect { |p| p.id } + end + + def test_eager_with_has_many_and_limit_and_conditions_array + posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "posts.body = ?", 'hello' ], :order => "posts.id") + assert_equal 2, posts.size + assert_equal [4,5], posts.collect { |p| p.id } + end + + def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers + posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ]) + assert_equal 2, posts.size + + count = Post.count(:include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ]) + assert_equal count, posts.size + end + + def test_eager_with_has_many_and_limit_ond_high_offset + posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ]) + assert_equal 0, posts.size + end + + def test_count_eager_with_has_many_and_limit_ond_high_offset + posts = Post.count(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ]) + assert_equal 0, posts + end + + def test_eager_with_has_many_and_limit_with_no_results + posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.title = 'magic forest'") + assert_equal 0, posts.size + end + + def test_eager_with_has_and_belongs_to_many_and_limit + posts = Post.find(:all, :include => :categories, :order => "posts.id", :limit => 3) + assert_equal 3, posts.size + assert_equal 2, posts[0].categories.size + assert_equal 1, posts[1].categories.size + assert_equal 0, posts[2].categories.size + assert posts[0].categories.include?(categories(:technology)) + assert posts[1].categories.include?(categories(:general)) + end + + def test_eager_with_has_many_and_limit_and_conditions_on_the_eagers + posts = authors(:david).posts.find(:all, + :include => :comments, + :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'", + :limit => 2 + ) + assert_equal 2, posts.size + + count = Post.count( + :include => [ :comments, :author ], + :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')", + :limit => 2 + ) + assert_equal count, posts.size + end + + def test_eager_with_has_many_and_limit_and_scoped_conditions_on_the_eagers + posts = nil + Post.with_scope(:find => { + :include => :comments, + :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'" + }) do + posts = authors(:david).posts.find(:all, :limit => 2) + assert_equal 2, posts.size + end + + Post.with_scope(:find => { + :include => [ :comments, :author ], + :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')" + }) do + count = Post.count(:limit => 2) + assert_equal count, posts.size + end + end + + def test_eager_with_has_many_and_limit_and_scoped_and_explicit_conditions_on_the_eagers + Post.with_scope(:find => { :conditions => "1=1" }) do + posts = authors(:david).posts.find(:all, + :include => :comments, + :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'", + :limit => 2 + ) + assert_equal 2, posts.size + + count = Post.count( + :include => [ :comments, :author ], + :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')", + :limit => 2 + ) + assert_equal count, posts.size + end + end + def test_eager_association_loading_with_habtm + posts = Post.find(:all, :include => :categories, :order => "posts.id") + assert_equal 2, posts[0].categories.size + assert_equal 1, posts[1].categories.size + assert_equal 0, posts[2].categories.size + assert posts[0].categories.include?(categories(:technology)) + assert posts[1].categories.include?(categories(:general)) + end + + def test_eager_with_inheritance + posts = SpecialPost.find(:all, :include => [ :comments ]) + end + + def test_eager_has_one_with_association_inheritance + post = Post.find(4, :include => [ :very_special_comment ]) + assert_equal "VerySpecialComment", post.very_special_comment.class.to_s + end + + def test_eager_has_many_with_association_inheritance + post = Post.find(4, :include => [ :special_comments ]) + post.special_comments.each do |special_comment| + assert_equal "SpecialComment", special_comment.class.to_s + end + end + + def test_eager_habtm_with_association_inheritance + post = Post.find(6, :include => [ :special_categories ]) + assert_equal 1, post.special_categories.size + post.special_categories.each do |special_category| + assert_equal "SpecialCategory", special_category.class.to_s + end + end + + def test_eager_with_has_one_dependent_does_not_destroy_dependent + assert_not_nil companies(:first_firm).account + f = Firm.find(:first, :include => :account, + :conditions => ["companies.name = ?", "37signals"]) + assert_not_nil f.account + assert_equal companies(:first_firm, :reload).account, f.account + end + + def test_eager_with_invalid_association_reference + assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + post = Post.find(6, :include=> :monkeys ) + } + assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + post = Post.find(6, :include=>[ :monkeys ]) + } + assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + post = Post.find(6, :include=>[ 'monkeys' ]) + } + assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") { + post = Post.find(6, :include=>[ :monkeys, :elephants ]) + } + end + + def find_all_ordered(className, include=nil) + className.find(:all, :order=>"#{className.table_name}.#{className.primary_key}", :include=>include) + end + + def test_eager_with_multiple_associations_with_same_table_has_many_and_habtm + # Eager includes of has many and habtm associations aren't necessarily sorted in the same way + def assert_equal_after_sort(item1, item2, item3 = nil) + assert_equal(item1.sort{|a,b| a.id <=> b.id}, item2.sort{|a,b| a.id <=> b.id}) + assert_equal(item3.sort{|a,b| a.id <=> b.id}, item2.sort{|a,b| a.id <=> b.id}) if item3 + end + # Test regular association, association with conditions, association with + # STI, and association with conditions assured not to be true + post_types = [:posts, :hello_posts, :special_posts, :nonexistent_posts] + # test both has_many and has_and_belongs_to_many + [Author, Category].each do |className| + d1 = find_all_ordered(className) + # test including all post types at once + d2 = find_all_ordered(className, post_types) + d1.each_index do |i| + assert_equal(d1[i], d2[i]) + assert_equal_after_sort(d1[i].posts, d2[i].posts) + post_types[1..-1].each do |post_type| + # test including post_types together + d3 = find_all_ordered(className, [:posts, post_type]) + assert_equal(d1[i], d3[i]) + assert_equal_after_sort(d1[i].posts, d3[i].posts) + assert_equal_after_sort(d1[i].send(post_type), d2[i].send(post_type), d3[i].send(post_type)) + end + end + end + end + + def test_eager_with_multiple_associations_with_same_table_has_one + d1 = find_all_ordered(Firm) + d2 = find_all_ordered(Firm, :account) + d1.each_index do |i| + assert_equal(d1[i], d2[i]) + assert_equal(d1[i].account, d2[i].account) + end + end + + def test_eager_with_multiple_associations_with_same_table_belongs_to + firm_types = [:firm, :firm_with_basic_id, :firm_with_other_name, :firm_with_condition] + d1 = find_all_ordered(Client) + d2 = find_all_ordered(Client, firm_types) + d1.each_index do |i| + assert_equal(d1[i], d2[i]) + firm_types.each { |type| assert_equal(d1[i].send(type), d2[i].send(type)) } + end + end + def test_eager_with_valid_association_as_string_not_symbol + assert_nothing_raised { Post.find(:all, :include => 'comments') } + end + + def test_preconfigured_includes_with_belongs_to + author = posts(:welcome).author_with_posts + assert_equal 5, author.posts.size + end + + def test_preconfigured_includes_with_has_one + comment = posts(:sti_comments).very_special_comment_with_post + assert_equal posts(:sti_comments), comment.post + end + + def test_preconfigured_includes_with_has_many + posts = authors(:david).posts_with_comments + one = posts.detect { |p| p.id == 1 } + assert_equal 5, posts.size + assert_equal 2, one.comments.size + end + + def test_preconfigured_includes_with_habtm + posts = authors(:david).posts_with_categories + one = posts.detect { |p| p.id == 1 } + assert_equal 5, posts.size + assert_equal 2, one.categories.size + end + + def test_preconfigured_includes_with_has_many_and_habtm + posts = authors(:david).posts_with_comments_and_categories + one = posts.detect { |p| p.id == 1 } + assert_equal 5, posts.size + assert_equal 2, one.comments.size + assert_equal 2, one.categories.size + end +end diff --git a/vendor/rails/activerecord/test/associations_join_model_test.rb b/vendor/rails/activerecord/test/associations_join_model_test.rb new file mode 100644 index 00000000..93cfd008 --- /dev/null +++ b/vendor/rails/activerecord/test/associations_join_model_test.rb @@ -0,0 +1,370 @@ +require 'abstract_unit' +require 'fixtures/tag' +require 'fixtures/tagging' +require 'fixtures/post' +require 'fixtures/comment' +require 'fixtures/author' +require 'fixtures/category' +require 'fixtures/categorization' + +class AssociationsJoinModelTest < Test::Unit::TestCase + self.use_transactional_fixtures = false + fixtures :posts, :authors, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites + + def test_has_many + assert_equal categories(:general), authors(:david).categories.first + end + + def test_has_many_inherited + assert_equal categories(:sti_test), authors(:mary).categories.first + end + + def test_inherited_has_many + assert_equal authors(:mary), categories(:sti_test).authors.first + end + + def test_polymorphic_has_many + assert_equal taggings(:welcome_general), posts(:welcome).taggings.first + end + + def test_polymorphic_has_one + assert_equal taggings(:welcome_general), posts(:welcome).tagging + end + + def test_polymorphic_belongs_to + assert_equal posts(:welcome), posts(:welcome).taggings.first.taggable + end + + def test_polymorphic_has_many_going_through_join_model + assert_equal tags(:general), tag = posts(:welcome).tags.first + assert_no_queries do + tag.tagging + end + end + + def test_count_polymorphic_has_many + assert_equal 1, posts(:welcome).taggings.count + assert_equal 1, posts(:welcome).tags.count + end + + def test_polymorphic_has_many_going_through_join_model_with_find + assert_equal tags(:general), tag = posts(:welcome).tags.find(:first) + assert_no_queries do + tag.tagging + end + end + + def test_polymorphic_has_many_going_through_join_model_with_include_on_source_reflection + assert_equal tags(:general), tag = posts(:welcome).funky_tags.first + assert_no_queries do + tag.tagging + end + end + + def test_polymorphic_has_many_going_through_join_model_with_include_on_source_reflection_with_find + assert_equal tags(:general), tag = posts(:welcome).funky_tags.find(:first) + assert_no_queries do + tag.tagging + end + end + + def test_polymorphic_has_many_going_through_join_model_with_disabled_include + assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first + assert_queries 1 do + tag.tagging + end + end + + def test_polymorphic_has_many_going_through_join_model_with_custom_select_and_joins + assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first + tag.author_id + end + + def test_polymorphic_has_many_going_through_join_model_with_custom_foreign_key + assert_equal tags(:misc), taggings(:welcome_general).super_tag + assert_equal tags(:misc), posts(:welcome).super_tags.first + end + + def test_polymorphic_has_many_create_model_with_inheritance_and_custom_base_class + post = SubStiPost.create :title => 'SubStiPost', :body => 'SubStiPost body' + assert_instance_of SubStiPost, post + + tagging = tags(:misc).taggings.create(:taggable => post) + assert_equal "SubStiPost", tagging.taggable_type + end + + def test_polymorphic_has_many_going_through_join_model_with_inheritance + assert_equal tags(:general), posts(:thinking).tags.first + end + + def test_polymorphic_has_many_going_through_join_model_with_inheritance_with_custom_class_name + assert_equal tags(:general), posts(:thinking).funky_tags.first + end + + def test_polymorphic_has_many_create_model_with_inheritance + post = posts(:thinking) + assert_instance_of SpecialPost, post + + tagging = tags(:misc).taggings.create(:taggable => post) + assert_equal "Post", tagging.taggable_type + end + + def test_polymorphic_has_one_create_model_with_inheritance + tagging = tags(:misc).create_tagging(:taggable => posts(:thinking)) + assert_equal "Post", tagging.taggable_type + end + + def test_set_polymorphic_has_many + tagging = tags(:misc).taggings.create + posts(:thinking).taggings << tagging + assert_equal "Post", tagging.taggable_type + end + + def test_set_polymorphic_has_one + tagging = tags(:misc).taggings.create + posts(:thinking).tagging = tagging + assert_equal "Post", tagging.taggable_type + end + + def test_create_polymorphic_has_many_with_scope + old_count = posts(:welcome).taggings.count + tagging = posts(:welcome).taggings.create(:tag => tags(:misc)) + assert_equal "Post", tagging.taggable_type + assert_equal old_count+1, posts(:welcome).taggings.count + end + + def test_create_polymorphic_has_one_with_scope + old_count = Tagging.count + tagging = posts(:welcome).tagging.create(:tag => tags(:misc)) + assert_equal "Post", tagging.taggable_type + assert_equal old_count+1, Tagging.count + end + + def test_delete_polymorphic_has_many_with_delete_all + assert_equal 1, posts(:welcome).taggings.count + posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyDeleteAll' + post = find_post_with_dependency(1, :has_many, :taggings, :delete_all) + + old_count = Tagging.count + post.destroy + assert_equal old_count-1, Tagging.count + assert_equal 0, posts(:welcome).taggings.count + end + + def test_delete_polymorphic_has_many_with_destroy + assert_equal 1, posts(:welcome).taggings.count + posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyDestroy' + post = find_post_with_dependency(1, :has_many, :taggings, :destroy) + + old_count = Tagging.count + post.destroy + assert_equal old_count-1, Tagging.count + assert_equal 0, posts(:welcome).taggings.count + end + + def test_delete_polymorphic_has_many_with_nullify + assert_equal 1, posts(:welcome).taggings.count + posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyNullify' + post = find_post_with_dependency(1, :has_many, :taggings, :nullify) + + old_count = Tagging.count + post.destroy + assert_equal old_count, Tagging.count + assert_equal 0, posts(:welcome).taggings.count + end + + def test_delete_polymorphic_has_one_with_destroy + assert posts(:welcome).tagging + posts(:welcome).tagging.update_attribute :taggable_type, 'PostWithHasOneDestroy' + post = find_post_with_dependency(1, :has_one, :tagging, :destroy) + + old_count = Tagging.count + post.destroy + assert_equal old_count-1, Tagging.count + assert_nil posts(:welcome).tagging(true) + end + + def test_delete_polymorphic_has_one_with_nullify + assert posts(:welcome).tagging + posts(:welcome).tagging.update_attribute :taggable_type, 'PostWithHasOneNullify' + post = find_post_with_dependency(1, :has_one, :tagging, :nullify) + + old_count = Tagging.count + post.destroy + assert_equal old_count, Tagging.count + assert_nil posts(:welcome).tagging(true) + end + + def test_has_many_with_piggyback + assert_equal "2", categories(:sti_test).authors.first.post_id.to_s + end + + def test_include_has_many_through + posts = Post.find(:all, :order => 'posts.id') + posts_with_authors = Post.find(:all, :include => :authors, :order => 'posts.id') + assert_equal posts.length, posts_with_authors.length + posts.length.times do |i| + assert_equal posts[i].authors.length, assert_no_queries { posts_with_authors[i].authors.length } + end + end + + def test_include_polymorphic_has_one + post = Post.find_by_id(posts(:welcome).id, :include => :tagging) + tagging = taggings(:welcome_general) + assert_no_queries do + assert_equal tagging, post.tagging + end + end + + def test_include_polymorphic_has_many_through + posts = Post.find(:all, :order => 'posts.id') + posts_with_tags = Post.find(:all, :include => :tags, :order => 'posts.id') + assert_equal posts.length, posts_with_tags.length + posts.length.times do |i| + assert_equal posts[i].tags.length, assert_no_queries { posts_with_tags[i].tags.length } + end + end + + def test_include_polymorphic_has_many + posts = Post.find(:all, :order => 'posts.id') + posts_with_taggings = Post.find(:all, :include => :taggings, :order => 'posts.id') + assert_equal posts.length, posts_with_taggings.length + posts.length.times do |i| + assert_equal posts[i].taggings.length, assert_no_queries { posts_with_taggings[i].taggings.length } + end + end + + def test_has_many_find_all + assert_equal [categories(:general)], authors(:david).categories.find(:all) + end + + def test_has_many_find_first + assert_equal categories(:general), authors(:david).categories.find(:first) + end + + def test_has_many_find_conditions + assert_equal categories(:general), authors(:david).categories.find(:first, :conditions => "categories.name = 'General'") + assert_equal nil, authors(:david).categories.find(:first, :conditions => "categories.name = 'Technology'") + end + + def test_has_many_class_methods_called_by_method_missing + assert_equal categories(:general), authors(:david).categories.find_all_by_name('General').first +# assert_equal nil, authors(:david).categories.find_by_name('Technology') + end + + def test_has_many_going_through_join_model_with_custom_foreign_key + assert_equal [], posts(:thinking).authors + assert_equal [authors(:mary)], posts(:authorless).authors + end + + def test_belongs_to_polymorphic_with_counter_cache + assert_equal 0, posts(:welcome)[:taggings_count] + tagging = posts(:welcome).taggings.create(:tag => tags(:general)) + assert_equal 1, posts(:welcome, :reload)[:taggings_count] + tagging.destroy + assert posts(:welcome, :reload)[:taggings_count].zero? + end + + def test_unavailable_through_reflection + assert_raises (ActiveRecord::HasManyThroughAssociationNotFoundError) { authors(:david).nothings } + end + + def test_has_many_through_join_model_with_conditions + assert_equal [], posts(:welcome).invalid_taggings + assert_equal [], posts(:welcome).invalid_tags + end + + def test_has_many_polymorphic + assert_raises ActiveRecord::HasManyThroughAssociationPolymorphicError do + assert_equal [posts(:welcome), posts(:thinking)], tags(:general).taggables + end + assert_raises ActiveRecord::EagerLoadPolymorphicError do + assert_equal [posts(:welcome), posts(:thinking)], tags(:general).taggings.find(:all, :include => :taggable) + end + end + + def test_has_many_through_has_many_find_all + assert_equal comments(:greetings), authors(:david).comments.find(:all, :order => 'comments.id').first + end + + def test_has_many_through_has_many_find_all_with_custom_class + assert_equal comments(:greetings), authors(:david).funky_comments.find(:all, :order => 'comments.id').first + end + + def test_has_many_through_has_many_find_first + assert_equal comments(:greetings), authors(:david).comments.find(:first, :order => 'comments.id') + end + + def test_has_many_through_has_many_find_conditions + assert_equal comments(:does_it_hurt), authors(:david).comments.find(:first, :conditions => "comments.type='SpecialComment'", :order => 'comments.id') + end + + def test_has_many_through_has_many_find_by_id + assert_equal comments(:more_greetings), authors(:david).comments.find(2) + end + + def test_has_many_through_polymorphic_has_one + assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tagging } + end + + def test_has_many_through_polymorphic_has_many + assert_equal [taggings(:welcome_general), taggings(:thinking_general)], authors(:david).taggings.uniq.sort_by { |t| t.id } + end + + def test_include_has_many_through_polymorphic_has_many + author = Author.find_by_id(authors(:david).id, :include => :taggings) + expected_taggings = [taggings(:welcome_general), taggings(:thinking_general)] + assert_no_queries do + assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id } + end + end + + def test_has_many_through_has_many_through + assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tags } + end + + def test_has_many_through_habtm + assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).post_categories } + end + + def test_eager_load_has_many_through_has_many + author = Author.find :first, :conditions => ['name = ?', 'David'], :include => :comments, :order => 'comments.id' + SpecialComment.new; VerySpecialComment.new + assert_no_queries do + assert_equal [1,2,3,5,6,7,8,9,10], author.comments.collect(&:id) + end + end + + def test_eager_belongs_to_and_has_one_not_singularized + assert_nothing_raised do + Author.find(:first, :include => :author_address) + AuthorAddress.find(:first, :include => :author) + end + end + + def test_self_referential_has_many_through + assert_equal [authors(:mary)], authors(:david).favorite_authors + assert_equal [], authors(:mary).favorite_authors + end + + def test_add_to_self_referential_has_many_through + new_author = Author.create(:name => "Bob") + authors(:david).author_favorites.create :favorite_author => new_author + assert_equal new_author, authors(:david).reload.favorite_authors.first + end + + def test_has_many_through_uses_correct_attributes + assert_nil posts(:thinking).tags.find_by_name("General").attributes["tag_id"] + end + + private + # create dynamic Post models to allow different dependency options + def find_post_with_dependency(post_id, association, association_name, dependency) + class_name = "PostWith#{association.to_s.classify}#{dependency.to_s.classify}" + Post.find(post_id).update_attribute :type, class_name + klass = Object.const_set(class_name, Class.new(ActiveRecord::Base)) + klass.set_table_name 'posts' + klass.send(association, association_name, :as => :taggable, :dependent => dependency) + klass.find(post_id) + end +end diff --git a/vendor/rails/activerecord/test/associations_test.rb b/vendor/rails/activerecord/test/associations_test.rb new file mode 100755 index 00000000..173808c3 --- /dev/null +++ b/vendor/rails/activerecord/test/associations_test.rb @@ -0,0 +1,1515 @@ +require 'abstract_unit' +require 'fixtures/developer' +require 'fixtures/project' +require 'fixtures/company' +require 'fixtures/topic' +require 'fixtures/reply' +require 'fixtures/computer' +require 'fixtures/customer' +require 'fixtures/order' +require 'fixtures/category' +require 'fixtures/post' +require 'fixtures/author' + +# Can't declare new classes in test case methods, so tests before that +bad_collection_keys = false +begin + class Car < ActiveRecord::Base; has_many :wheels, :name => "wheels"; end +rescue ArgumentError + bad_collection_keys = true +end +raise "ActiveRecord should have barked on bad collection keys" unless bad_collection_keys + + +class AssociationsTest < Test::Unit::TestCase + fixtures :accounts, :companies, :developers, :projects, :developers_projects, + :computers + + def test_force_reload + firm = Firm.new("name" => "A New Firm, Inc") + firm.save + firm.clients.each {|c|} # forcing to load all clients + assert firm.clients.empty?, "New firm shouldn't have client objects" + assert !firm.has_clients?, "New firm shouldn't have clients" + assert_equal 0, firm.clients.size, "New firm should have 0 clients" + + client = Client.new("name" => "TheClient.com", "firm_id" => firm.id) + client.save + + assert firm.clients.empty?, "New firm should have cached no client objects" + assert !firm.has_clients?, "New firm should have cached a no-clients response" + assert_equal 0, firm.clients.size, "New firm should have cached 0 clients count" + + assert !firm.clients(true).empty?, "New firm should have reloaded client objects" + assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count" + end + + def test_storing_in_pstore + require "tmpdir" + store_filename = File.join(Dir.tmpdir, "ar-pstore-association-test") + File.delete(store_filename) if File.exists?(store_filename) + require "pstore" + apple = Firm.create("name" => "Apple") + natural = Client.new("name" => "Natural Company") + apple.clients << natural + + db = PStore.new(store_filename) + db.transaction do + db["apple"] = apple + end + + db = PStore.new(store_filename) + db.transaction do + assert_equal "Natural Company", db["apple"].clients.first.name + end + end +end + +class HasOneAssociationsTest < Test::Unit::TestCase + fixtures :accounts, :companies, :developers, :projects, :developers_projects + + def test_has_one + assert_equal companies(:first_firm).account, Account.find(1) + assert_equal Account.find(1).credit_limit, companies(:first_firm).account.credit_limit + end + + def test_proxy_assignment + company = companies(:first_firm) + assert_nothing_raised { company.account = company.account } + end + + def test_triple_equality + assert Account === companies(:first_firm).account + assert companies(:first_firm).account === Account + end + + def test_type_mismatch + assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = 1 } + assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = Project.find(1) } + end + + def test_natural_assignment + apple = Firm.create("name" => "Apple") + citibank = Account.create("credit_limit" => 10) + apple.account = citibank + assert_equal apple.id, citibank.firm_id + end + + def test_natural_assignment_to_nil + old_account_id = companies(:first_firm).account.id + companies(:first_firm).account = nil + companies(:first_firm).save + assert_nil companies(:first_firm).account + # account is dependent, therefore is destroyed when reference to owner is lost + assert_raises(ActiveRecord::RecordNotFound) { Account.find(old_account_id) } + end + + def test_assignment_without_replacement + apple = Firm.create("name" => "Apple") + citibank = Account.create("credit_limit" => 10) + apple.account = citibank + assert_equal apple.id, citibank.firm_id + + hsbc = apple.build_account({ :credit_limit => 20}, false) + assert_equal apple.id, hsbc.firm_id + hsbc.save + assert_equal apple.id, citibank.firm_id + + nykredit = apple.create_account({ :credit_limit => 30}, false) + assert_equal apple.id, nykredit.firm_id + assert_equal apple.id, citibank.firm_id + assert_equal apple.id, hsbc.firm_id + end + + def test_assignment_without_replacement_on_create + apple = Firm.create("name" => "Apple") + citibank = Account.create("credit_limit" => 10) + apple.account = citibank + assert_equal apple.id, citibank.firm_id + + hsbc = apple.create_account({:credit_limit => 10}, false) + assert_equal apple.id, hsbc.firm_id + hsbc.save + assert_equal apple.id, citibank.firm_id + end + + def test_dependence + num_accounts = Account.count + firm = Firm.find(1) + assert !firm.account.nil? + firm.destroy + assert_equal num_accounts - 1, Account.count + end + + def test_succesful_build_association + firm = Firm.new("name" => "GlobalMegaCorp") + firm.save + + account = firm.build_account("credit_limit" => 1000) + assert account.save + assert_equal account, firm.account + end + + def test_failing_build_association + firm = Firm.new("name" => "GlobalMegaCorp") + firm.save + + account = firm.build_account + assert !account.save + assert_equal "can't be empty", account.errors.on("credit_limit") + end + + def test_build_association_twice_without_saving_affects_nothing + count_of_account = Account.count + firm = Firm.find(:first) + account1 = firm.build_account("credit_limit" => 1000) + account2 = firm.build_account("credit_limit" => 2000) + + assert_equal count_of_account, Account.count + end + + def test_create_association + firm = Firm.new("name" => "GlobalMegaCorp") + firm.save + assert_equal firm.create_account("credit_limit" => 1000), firm.account + end + + def test_build + firm = Firm.new("name" => "GlobalMegaCorp") + firm.save + + firm.account = account = Account.new("credit_limit" => 1000) + assert_equal account, firm.account + assert account.save + assert_equal account, firm.account + end + + def test_build_before_child_saved + firm = Firm.find(1) + + account = firm.account.build("credit_limit" => 1000) + assert_equal account, firm.account + assert account.new_record? + assert firm.save + assert_equal account, firm.account + assert !account.new_record? + end + + def test_build_before_either_saved + firm = Firm.new("name" => "GlobalMegaCorp") + + firm.account = account = Account.new("credit_limit" => 1000) + assert_equal account, firm.account + assert account.new_record? + assert firm.save + assert_equal account, firm.account + assert !account.new_record? + end + + def test_failing_build_association + firm = Firm.new("name" => "GlobalMegaCorp") + firm.save + + firm.account = account = Account.new + assert_equal account, firm.account + assert !account.save + assert_equal account, firm.account + assert_equal "can't be empty", account.errors.on("credit_limit") + end + + def test_create + firm = Firm.new("name" => "GlobalMegaCorp") + firm.save + firm.account = account = Account.create("credit_limit" => 1000) + assert_equal account, firm.account + end + + def test_create_before_save + firm = Firm.new("name" => "GlobalMegaCorp") + firm.account = account = Account.create("credit_limit" => 1000) + assert_equal account, firm.account + end + + def test_dependence_with_missing_association + Account.destroy_all + firm = Firm.find(1) + assert firm.account.nil? + firm.destroy + end + + def test_assignment_before_parent_saved + firm = Firm.new("name" => "GlobalMegaCorp") + firm.account = a = Account.find(1) + assert firm.new_record? + assert_equal a, firm.account + assert firm.save + assert_equal a, firm.account + assert_equal a, firm.account(true) + end + + def test_assignment_before_child_saved + firm = Firm.find(1) + firm.account = a = Account.new("credit_limit" => 1000) + assert !a.new_record? + assert_equal a, firm.account + assert_equal a, firm.account + assert_equal a, firm.account(true) + end + + def test_assignment_before_either_saved + firm = Firm.new("name" => "GlobalMegaCorp") + firm.account = a = Account.new("credit_limit" => 1000) + assert firm.new_record? + assert a.new_record? + assert_equal a, firm.account + assert firm.save + assert !firm.new_record? + assert !a.new_record? + assert_equal a, firm.account + assert_equal a, firm.account(true) + end +end + + +class HasManyAssociationsTest < Test::Unit::TestCase + fixtures :accounts, :companies, :developers, :projects, + :developers_projects, :topics + + def setup + Client.destroyed_client_ids.clear + end + + def force_signal37_to_load_all_clients_of_firm + companies(:first_firm).clients_of_firm.each {|f| } + end + + def test_counting + assert_equal 2, Firm.find(:first).clients.count + end + + def test_finding + assert_equal 2, Firm.find(:first).clients.length + end + + def test_find_many_with_merged_options + assert_equal 1, companies(:first_firm).limited_clients.size + assert_equal 1, companies(:first_firm).limited_clients.find(:all).size + assert_equal 2, companies(:first_firm).limited_clients.find(:all, :limit => nil).size + end + + def test_triple_equality + assert !(Array === Firm.find(:first).clients) + assert Firm.find(:first).clients === Array + end + + def test_finding_default_orders + assert_equal "Summit", Firm.find(:first).clients.first.name + end + + def test_finding_with_different_class_name_and_order + assert_equal "Microsoft", Firm.find(:first).clients_sorted_desc.first.name + end + + def test_finding_with_foreign_key + assert_equal "Microsoft", Firm.find(:first).clients_of_firm.first.name + end + + def test_finding_with_condition + assert_equal "Microsoft", Firm.find(:first).clients_like_ms.first.name + end + + def test_finding_using_sql + firm = Firm.find(:first) + first_client = firm.clients_using_sql.first + assert_not_nil first_client + assert_equal "Microsoft", first_client.name + assert_equal 1, firm.clients_using_sql.size + assert_equal 1, Firm.find(:first).clients_using_sql.size + end + + def test_counting_using_sql + assert_equal 1, Firm.find(:first).clients_using_counter_sql.size + assert_equal 0, Firm.find(:first).clients_using_zero_counter_sql.size + end + + def test_counting_non_existant_items_using_sql + assert_equal 0, Firm.find(:first).no_clients_using_counter_sql.size + end + + def test_belongs_to_sanity + c = Client.new + assert_nil c.firm + + if c.firm + assert false, "belongs_to failed if check" + end + + unless c.firm + else + assert false, "belongs_to failed unless check" + end + end + + def test_find_ids + firm = Firm.find(:first) + + assert_raises(ActiveRecord::RecordNotFound) { firm.clients.find } + + client = firm.clients.find(2) + assert_kind_of Client, client + + client_ary = firm.clients.find([2]) + assert_kind_of Array, client_ary + assert_equal client, client_ary.first + + client_ary = firm.clients.find(2, 3) + assert_kind_of Array, client_ary + assert_equal 2, client_ary.size + assert_equal client, client_ary.first + + assert_raises(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) } + end + + def test_find_all + firm = Firm.find_first + assert_equal firm.clients, firm.clients.find_all + assert_equal 2, firm.clients.find(:all, :conditions => "#{QUOTED_TYPE} = 'Client'").length + assert_equal 1, firm.clients.find(:all, :conditions => "name = 'Summit'").length + end + + def test_find_all_sanitized + firm = Firm.find_first + assert_equal firm.clients.find_all("name = 'Summit'"), firm.clients.find_all(["name = '%s'", "Summit"]) + summit = firm.clients.find(:all, :conditions => "name = 'Summit'") + assert_equal summit, firm.clients.find(:all, :conditions => ["name = ?", "Summit"]) + assert_equal summit, firm.clients.find(:all, :conditions => ["name = :name", { :name => "Summit" }]) + end + + def test_find_first + firm = Firm.find_first + client2 = Client.find(2) + assert_equal firm.clients.first, firm.clients.find_first + assert_equal client2, firm.clients.find_first("#{QUOTED_TYPE} = 'Client'") + assert_equal client2, firm.clients.find(:first, :conditions => "#{QUOTED_TYPE} = 'Client'") + end + + def test_find_first_sanitized + firm = Firm.find_first + client2 = Client.find(2) + assert_equal client2, firm.clients.find_first(["#{QUOTED_TYPE} = ?", "Client"]) + assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = ?", 'Client']) + assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }]) + end + + def test_find_in_collection + assert_equal Client.find(2).name, companies(:first_firm).clients.find(2).name + assert_raises(ActiveRecord::RecordNotFound) { companies(:first_firm).clients.find(6) } + end + + def test_find_grouped + all_clients_of_firm1 = Client.find(:all, :conditions => "firm_id = 1") + grouped_clients_of_firm1 = Client.find(:all, :conditions => "firm_id = 1", :group => "firm_id", :select => 'firm_id, count(id) as clients_count') + assert_equal 2, all_clients_of_firm1.size + assert_equal 1, grouped_clients_of_firm1.size + end + + def test_adding + force_signal37_to_load_all_clients_of_firm + natural = Client.new("name" => "Natural Company") + companies(:first_firm).clients_of_firm << natural + assert_equal 2, companies(:first_firm).clients_of_firm.size # checking via the collection + assert_equal 2, companies(:first_firm).clients_of_firm(true).size # checking using the db + assert_equal natural, companies(:first_firm).clients_of_firm.last + end + + 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 << 1 } + assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << Topic.find(1) } + end + + def test_adding_a_collection + force_signal37_to_load_all_clients_of_firm + companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")]) + assert_equal 3, companies(:first_firm).clients_of_firm.size + assert_equal 3, companies(:first_firm).clients_of_firm(true).size + end + + def test_adding_before_save + no_of_firms = Firm.count + no_of_clients = Client.count + new_firm = Firm.new("name" => "A New Firm, Inc") + new_firm.clients_of_firm.push Client.new("name" => "Natural Company") + new_firm.clients_of_firm << (c = Client.new("name" => "Apple")) + assert new_firm.new_record? + assert c.new_record? + assert_equal 2, new_firm.clients_of_firm.size + assert_equal no_of_firms, Firm.count # Firm was not saved to database. + assert_equal no_of_clients, Client.count # Clients were not saved to database. + assert new_firm.save + assert !new_firm.new_record? + assert !c.new_record? + assert_equal new_firm, c.firm + assert_equal no_of_firms+1, Firm.count # Firm was saved to database. + assert_equal no_of_clients+2, Client.count # Clients were saved to database. + assert_equal 2, new_firm.clients_of_firm.size + assert_equal 2, new_firm.clients_of_firm(true).size + end + + def test_invalid_adding + firm = Firm.find(1) + assert !(firm.clients_of_firm << c = Client.new) + assert c.new_record? + assert !firm.valid? + assert !firm.save + assert c.new_record? + end + + def test_invalid_adding_before_save + no_of_firms = Firm.count + no_of_clients = Client.count + new_firm = Firm.new("name" => "A New Firm, Inc") + new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")]) + assert c.new_record? + assert !c.valid? + assert !new_firm.valid? + assert !new_firm.save + assert c.new_record? + assert new_firm.new_record? + end + + def test_build + new_client = companies(:first_firm).clients_of_firm.build("name" => "Another Client") + assert_equal "Another Client", new_client.name + assert new_client.new_record? + assert_equal new_client, companies(:first_firm).clients_of_firm.last + assert companies(:first_firm).save + assert !new_client.new_record? + assert_equal 2, companies(:first_firm).clients_of_firm(true).size + end + + def test_build_many + new_clients = companies(:first_firm).clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) + assert_equal 2, new_clients.size + + assert companies(:first_firm).save + assert_equal 3, companies(:first_firm).clients_of_firm(true).size + end + + def test_invalid_build + new_client = companies(:first_firm).clients_of_firm.build + assert new_client.new_record? + assert !new_client.valid? + assert_equal new_client, companies(:first_firm).clients_of_firm.last + assert !companies(:first_firm).save + assert new_client.new_record? + assert_equal 1, companies(:first_firm).clients_of_firm(true).size + end + + def test_create + force_signal37_to_load_all_clients_of_firm + new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client") + assert !new_client.new_record? + assert_equal new_client, companies(:first_firm).clients_of_firm.last + assert_equal new_client, companies(:first_firm).clients_of_firm(true).last + end + + def test_create_many + companies(:first_firm).clients_of_firm.create([{"name" => "Another Client"}, {"name" => "Another Client II"}]) + assert_equal 3, companies(:first_firm).clients_of_firm(true).size + end + + def test_find_or_create + number_of_clients = companies(:first_firm).clients.size + the_client = companies(:first_firm).clients.find_or_create_by_name("Yet another client") + assert_equal number_of_clients + 1, companies(:first_firm, :refresh).clients.size + assert_equal the_client, companies(:first_firm).clients.find_or_create_by_name("Yet another client") + assert_equal number_of_clients + 1, companies(:first_firm, :refresh).clients.size + end + + def test_deleting + force_signal37_to_load_all_clients_of_firm + companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first) + assert_equal 0, companies(:first_firm).clients_of_firm.size + assert_equal 0, companies(:first_firm).clients_of_firm(true).size + end + + def test_deleting_before_save + new_firm = Firm.new("name" => "A New Firm, Inc.") + new_client = new_firm.clients_of_firm.build("name" => "Another Client") + assert_equal 1, new_firm.clients_of_firm.size + new_firm.clients_of_firm.delete(new_client) + assert_equal 0, new_firm.clients_of_firm.size + end + + def test_deleting_a_collection + force_signal37_to_load_all_clients_of_firm + companies(:first_firm).clients_of_firm.create("name" => "Another Client") + assert_equal 2, companies(:first_firm).clients_of_firm.size + companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1]]) + assert_equal 0, companies(:first_firm).clients_of_firm.size + assert_equal 0, companies(:first_firm).clients_of_firm(true).size + end + + def test_delete_all + force_signal37_to_load_all_clients_of_firm + companies(:first_firm).clients_of_firm.create("name" => "Another Client") + assert_equal 2, companies(:first_firm).clients_of_firm.size + companies(:first_firm).clients_of_firm.delete_all + assert_equal 0, companies(:first_firm).clients_of_firm.size + assert_equal 0, companies(:first_firm).clients_of_firm(true).size + end + + def test_delete_all_with_not_yet_loaded_association_collection + force_signal37_to_load_all_clients_of_firm + companies(:first_firm).clients_of_firm.create("name" => "Another Client") + assert_equal 2, companies(:first_firm).clients_of_firm.size + companies(:first_firm).clients_of_firm.reset + companies(:first_firm).clients_of_firm.delete_all + assert_equal 0, companies(:first_firm).clients_of_firm.size + assert_equal 0, companies(:first_firm).clients_of_firm(true).size + end + + def test_clearing_an_association_collection + firm = companies(:first_firm) + client_id = firm.clients_of_firm.first.id + assert_equal 1, firm.clients_of_firm.size + + firm.clients_of_firm.clear + + assert_equal 0, firm.clients_of_firm.size + assert_equal 0, firm.clients_of_firm(true).size + assert_equal [], Client.destroyed_client_ids[firm.id] + + # Should not be destroyed since the association is not dependent. + assert_nothing_raised do + assert Client.find(client_id).firm.nil? + end + end + + def test_clearing_a_dependent_association_collection + firm = companies(:first_firm) + client_id = firm.dependent_clients_of_firm.first.id + assert_equal 1, firm.dependent_clients_of_firm.size + + # :dependent means destroy is called on each client + firm.dependent_clients_of_firm.clear + + assert_equal 0, firm.dependent_clients_of_firm.size + assert_equal 0, firm.dependent_clients_of_firm(true).size + assert_equal [client_id], Client.destroyed_client_ids[firm.id] + + # Should be destroyed since the association is dependent. + assert Client.find_by_id(client_id).nil? + end + + def test_clearing_an_exclusively_dependent_association_collection + firm = companies(:first_firm) + client_id = firm.exclusively_dependent_clients_of_firm.first.id + assert_equal 1, firm.exclusively_dependent_clients_of_firm.size + + assert_equal [], Client.destroyed_client_ids[firm.id] + + # :exclusively_dependent means each client is deleted directly from + # the database without looping through them calling destroy. + firm.exclusively_dependent_clients_of_firm.clear + + assert_equal 0, firm.exclusively_dependent_clients_of_firm.size + assert_equal 0, firm.exclusively_dependent_clients_of_firm(true).size + assert_equal [3], Client.destroyed_client_ids[firm.id] + + # Should be destroyed since the association is exclusively dependent. + assert Client.find_by_id(client_id).nil? + end + + def test_clearing_without_initial_access + firm = companies(:first_firm) + + firm.clients_of_firm.clear + + assert_equal 0, firm.clients_of_firm.size + assert_equal 0, firm.clients_of_firm(true).size + end + + def test_deleting_a_item_which_is_not_in_the_collection + force_signal37_to_load_all_clients_of_firm + summit = Client.find_first("name = 'Summit'") + companies(:first_firm).clients_of_firm.delete(summit) + assert_equal 1, companies(:first_firm).clients_of_firm.size + assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 2, summit.client_of + end + + def test_deleting_type_mismatch + david = Developer.find(1) + david.projects.reload + assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(1) } + end + + def test_deleting_self_type_mismatch + david = Developer.find(1) + david.projects.reload + assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(Project.find(1).developers) } + end + + def test_destroy_all + force_signal37_to_load_all_clients_of_firm + assert !companies(:first_firm).clients_of_firm.empty?, "37signals has clients after load" + companies(:first_firm).clients_of_firm.destroy_all + assert companies(:first_firm).clients_of_firm.empty?, "37signals has no clients after destroy all" + assert companies(:first_firm).clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh" + end + + def test_dependence + firm = companies(:first_firm) + assert_equal 2, firm.clients.size + firm.destroy + assert Client.find(:all, :conditions => "firm_id=#{firm.id}").empty? + end + + def test_destroy_dependent_when_deleted_from_association + firm = Firm.find(:first) + assert_equal 2, firm.clients.size + + client = firm.clients.first + firm.clients.delete(client) + + assert_raise(ActiveRecord::RecordNotFound) { Client.find(client.id) } + assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(client.id) } + assert_equal 1, firm.clients.size + end + + def test_three_levels_of_dependence + topic = Topic.create "title" => "neat and simple" + reply = topic.replies.create "title" => "neat and simple", "content" => "still digging it" + silly_reply = reply.replies.create "title" => "neat and simple", "content" => "ain't complaining" + + assert_nothing_raised { topic.destroy } + end + + uses_transaction :test_dependence_with_transaction_support_on_failure + def test_dependence_with_transaction_support_on_failure + firm = companies(:first_firm) + clients = firm.clients + assert_equal 2, clients.length + clients.last.instance_eval { def before_destroy() raise "Trigger rollback" end } + + firm.destroy rescue "do nothing" + + assert_equal 2, Client.find(:all, :conditions => "firm_id=#{firm.id}").size + end + + def test_dependence_on_account + num_accounts = Account.count + companies(:first_firm).destroy + assert_equal num_accounts - 1, Account.count + end + + def test_depends_and_nullify + num_accounts = Account.count + num_companies = Company.count + + core = companies(:rails_core) + assert_equal accounts(:rails_core_account), core.account + assert_equal [companies(:leetsoft), companies(:jadedpixel)], core.companies + core.destroy + assert_nil accounts(:rails_core_account).reload.firm_id + assert_nil companies(:leetsoft).reload.client_of + assert_nil companies(:jadedpixel).reload.client_of + + + assert_equal num_accounts, Account.count + end + + def test_included_in_collection + assert companies(:first_firm).clients.include?(Client.find(2)) + end + + def test_adding_array_and_collection + assert_nothing_raised { Firm.find(:first).clients + Firm.find(:all).last.clients } + end + + def test_find_all_without_conditions + firm = companies(:first_firm) + assert_equal 2, firm.clients.find(:all).length + end + + def test_replace_with_less + firm = Firm.find(:first) + firm.clients = [companies(:first_client)] + assert firm.save, "Could not save firm" + firm.reload + assert_equal 1, firm.clients.length + end + + + def test_replace_with_new + firm = Firm.find(:first) + new_client = Client.new("name" => "New Client") + firm.clients = [companies(:second_client),new_client] + firm.save + firm.reload + assert_equal 2, firm.clients.length + assert !firm.clients.include?(:first_client) + end + + def test_replace_on_new_object + firm = Firm.new("name" => "New Firm") + firm.clients = [companies(:second_client), Client.new("name" => "New Client")] + assert firm.save + firm.reload + assert_equal 2, firm.clients.length + assert firm.clients.include?(Client.find_by_name("New Client")) + end + + def test_assign_ids + firm = Firm.new("name" => "Apple") + firm.client_ids = [companies(:first_client).id, companies(:second_client).id] + firm.save + firm.reload + assert_equal 2, firm.clients.length + assert firm.clients.include?(companies(:second_client)) + end +end + +class BelongsToAssociationsTest < Test::Unit::TestCase + fixtures :accounts, :companies, :developers, :projects, :topics, + :developers_projects, :computers, :authors, :posts + + def test_belongs_to + Client.find(3).firm.name + assert_equal companies(:first_firm).name, Client.find(3).firm.name + assert !Client.find(3).firm.nil?, "Microsoft should have a firm" + end + + def test_proxy_assignment + account = Account.find(1) + assert_nothing_raised { account.firm = account.firm } + end + + def test_triple_equality + assert Client.find(3).firm === Firm + assert Firm === Client.find(3).firm + end + + def test_type_mismatch + assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = 1 } + assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = Project.find(1) } + end + + def test_natural_assignment + apple = Firm.create("name" => "Apple") + citibank = Account.create("credit_limit" => 10) + citibank.firm = apple + assert_equal apple.id, citibank.firm_id + end + + def test_creating_the_belonging_object + citibank = Account.create("credit_limit" => 10) + apple = citibank.create_firm("name" => "Apple") + assert_equal apple, citibank.firm + citibank.save + citibank.reload + assert_equal apple, citibank.firm + end + + def test_building_the_belonging_object + citibank = Account.create("credit_limit" => 10) + apple = citibank.build_firm("name" => "Apple") + citibank.save + assert_equal apple.id, citibank.firm_id + end + + def test_natural_assignment_to_nil + client = Client.find(3) + client.firm = nil + client.save + assert_nil client.firm(true) + assert_nil client.client_of + end + + def test_with_different_class_name + assert_equal Company.find(1).name, Company.find(3).firm_with_other_name.name + assert_not_nil Company.find(3).firm_with_other_name, "Microsoft should have a firm" + end + + def test_with_condition + assert_equal Company.find(1).name, Company.find(3).firm_with_condition.name + assert_not_nil Company.find(3).firm_with_condition, "Microsoft should have a firm" + end + + def test_belongs_to_counter + debate = Topic.create("title" => "debate") + assert_equal 0, debate.send(:read_attribute, "replies_count"), "No replies yet" + + trash = debate.replies.create("title" => "blah!", "content" => "world around!") + assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply created" + + trash.destroy + assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted" + end + + def test_belongs_to_counter_with_reassigning + t1 = Topic.create("title" => "t1") + t2 = Topic.create("title" => "t2") + r1 = Reply.new("title" => "r1", "content" => "r1") + r1.topic = t1 + + assert r1.save + assert_equal 1, Topic.find(t1.id).replies.size + assert_equal 0, Topic.find(t2.id).replies.size + + r1.topic = Topic.find(t2.id) + + assert r1.save + assert_equal 0, Topic.find(t1.id).replies.size + assert_equal 1, Topic.find(t2.id).replies.size + + r1.topic = nil + + assert_equal 0, Topic.find(t1.id).replies.size + assert_equal 0, Topic.find(t2.id).replies.size + + r1.topic = t1 + + assert_equal 1, Topic.find(t1.id).replies.size + assert_equal 0, Topic.find(t2.id).replies.size + + r1.destroy + + assert_equal 0, Topic.find(t1.id).replies.size + assert_equal 0, Topic.find(t2.id).replies.size + end + + def test_assignment_before_parent_saved + client = Client.find(:first) + apple = Firm.new("name" => "Apple") + client.firm = apple + assert_equal apple, client.firm + assert apple.new_record? + assert client.save + assert apple.save + assert !apple.new_record? + assert_equal apple, client.firm + assert_equal apple, client.firm(true) + end + + def test_assignment_before_child_saved + final_cut = Client.new("name" => "Final Cut") + firm = Firm.find(1) + final_cut.firm = firm + assert final_cut.new_record? + assert final_cut.save + assert !final_cut.new_record? + assert !firm.new_record? + assert_equal firm, final_cut.firm + assert_equal firm, final_cut.firm(true) + end + + def test_assignment_before_either_saved + final_cut = Client.new("name" => "Final Cut") + apple = Firm.new("name" => "Apple") + final_cut.firm = apple + assert final_cut.new_record? + assert apple.new_record? + assert final_cut.save + assert !final_cut.new_record? + assert !apple.new_record? + assert_equal apple, final_cut.firm + assert_equal apple, final_cut.firm(true) + end + + def test_new_record_with_foreign_key_but_no_object + c = Client.new("firm_id" => 1) + assert_equal Firm.find(:first), c.firm_with_basic_id + end + + def test_forgetting_the_load_when_foreign_key_enters_late + c = Client.new + assert_nil c.firm_with_basic_id + + c.firm_id = 1 + assert_equal Firm.find(:first), c.firm_with_basic_id + end + + def test_field_name_same_as_foreign_key + computer = Computer.find(1) + assert_not_nil computer.developer, ":foreign key == attribute didn't lock up" # ' + end + + def test_counter_cache + topic = Topic.create :title => "Zoom-zoom-zoom" + assert_equal 0, topic[:replies_count] + + reply = Reply.create(:title => "re: zoom", :content => "speedy quick!") + reply.topic = topic + + assert_equal 1, topic.reload[:replies_count] + assert_equal 1, topic.replies.size + + topic[:replies_count] = 15 + assert_equal 15, topic.replies.size + end + + def test_custom_counter_cache + reply = Reply.create(:title => "re: zoom", :content => "speedy quick!") + assert_equal 0, reply[:replies_count] + + silly = SillyReply.create(:title => "gaga", :content => "boo-boo") + silly.reply = reply + + assert_equal 1, reply.reload[:replies_count] + assert_equal 1, reply.replies.size + + reply[:replies_count] = 17 + assert_equal 17, reply.replies.size + end + + def test_store_two_association_with_one_save + num_orders = Order.count + num_customers = Customer.count + order = Order.new + + customer1 = order.billing = Customer.new + customer2 = order.shipping = Customer.new + assert order.save + assert_equal customer1, order.billing + assert_equal customer2, order.shipping + + order.reload + + assert_equal customer1, order.billing + assert_equal customer2, order.shipping + + assert_equal num_orders +1, Order.count + assert_equal num_customers +2, Customer.count + end + + + def test_store_association_in_two_relations_with_one_save + num_orders = Order.count + num_customers = Customer.count + order = Order.new + + customer = order.billing = order.shipping = Customer.new + assert order.save + assert_equal customer, order.billing + assert_equal customer, order.shipping + + order.reload + + assert_equal customer, order.billing + assert_equal customer, order.shipping + + assert_equal num_orders +1, Order.count + assert_equal num_customers +1, Customer.count + end + + def test_store_association_in_two_relations_with_one_save_in_existing_object + num_orders = Order.count + num_customers = Customer.count + order = Order.create + + customer = order.billing = order.shipping = Customer.new + assert order.save + assert_equal customer, order.billing + assert_equal customer, order.shipping + + order.reload + + assert_equal customer, order.billing + assert_equal customer, order.shipping + + assert_equal num_orders +1, Order.count + assert_equal num_customers +1, Customer.count + end + + def test_store_association_in_two_relations_with_one_save_in_existing_object_with_values + num_orders = Order.count + num_customers = Customer.count + order = Order.create + + customer = order.billing = order.shipping = Customer.new + assert order.save + assert_equal customer, order.billing + assert_equal customer, order.shipping + + order.reload + + customer = order.billing = order.shipping = Customer.new + + assert order.save + order.reload + + assert_equal customer, order.billing + assert_equal customer, order.shipping + + assert_equal num_orders +1, Order.count + assert_equal num_customers +2, Customer.count + end + + + def test_association_assignment_sticks + post = Post.find(:first) + + author1, author2 = Author.find(:all, :limit => 2) + assert_not_nil author1 + assert_not_nil author2 + + # make sure the association is loaded + post.author + + # set the association by id, directly + post.author_id = author2.id + + # save and reload + post.save! + post.reload + + # the author id of the post should be the id we set + assert_equal post.author_id, author2.id + end + +end + + +class ProjectWithAfterCreateHook < ActiveRecord::Base + set_table_name 'projects' + has_and_belongs_to_many :developers, + :class_name => "DeveloperForProjectWithAfterCreateHook", + :join_table => "developers_projects", + :foreign_key => "project_id", + :association_foreign_key => "developer_id" + + after_create :add_david + + def add_david + david = DeveloperForProjectWithAfterCreateHook.find_by_name('David') + david.projects << self + end +end + +class DeveloperForProjectWithAfterCreateHook < ActiveRecord::Base + set_table_name 'developers' + has_and_belongs_to_many :projects, + :class_name => "ProjectWithAfterCreateHook", + :join_table => "developers_projects", + :association_foreign_key => "project_id", + :foreign_key => "developer_id" +end + + +class HasAndBelongsToManyAssociationsTest < Test::Unit::TestCase + fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects + + def test_has_and_belongs_to_many + david = Developer.find(1) + + assert !david.projects.empty? + assert_equal 2, david.projects.size + + active_record = Project.find(1) + assert !active_record.developers.empty? + assert_equal 3, active_record.developers.size + assert active_record.developers.include?(david) + end + + def test_triple_equality + assert !(Array === Developer.find(1).projects) + assert Developer.find(1).projects === Array + end + + def test_adding_single + jamis = Developer.find(2) + jamis.projects.reload # causing the collection to load + action_controller = Project.find(2) + assert_equal 1, jamis.projects.size + assert_equal 1, action_controller.developers.size + + jamis.projects << action_controller + + assert_equal 2, jamis.projects.size + assert_equal 2, jamis.projects(true).size + assert_equal 2, action_controller.developers(true).size + end + + def test_adding_type_mismatch + jamis = Developer.find(2) + assert_raise(ActiveRecord::AssociationTypeMismatch) { jamis.projects << nil } + assert_raise(ActiveRecord::AssociationTypeMismatch) { jamis.projects << 1 } + end + + def test_adding_from_the_project + jamis = Developer.find(2) + action_controller = Project.find(2) + action_controller.developers.reload + assert_equal 1, jamis.projects.size + assert_equal 1, action_controller.developers.size + + action_controller.developers << jamis + + assert_equal 2, jamis.projects(true).size + assert_equal 2, action_controller.developers.size + assert_equal 2, action_controller.developers(true).size + end + + def test_adding_from_the_project_fixed_timestamp + jamis = Developer.find(2) + action_controller = Project.find(2) + action_controller.developers.reload + assert_equal 1, jamis.projects.size + assert_equal 1, action_controller.developers.size + updated_at = jamis.updated_at + + action_controller.developers << jamis + + assert_equal updated_at, jamis.updated_at + assert_equal 2, jamis.projects(true).size + assert_equal 2, action_controller.developers.size + assert_equal 2, action_controller.developers(true).size + end + + def test_adding_multiple + aredridel = Developer.new("name" => "Aredridel") + aredridel.save + aredridel.projects.reload + aredridel.projects.push(Project.find(1), Project.find(2)) + assert_equal 2, aredridel.projects.size + assert_equal 2, aredridel.projects(true).size + end + + def test_adding_a_collection + aredridel = Developer.new("name" => "Aredridel") + aredridel.save + aredridel.projects.reload + aredridel.projects.concat([Project.find(1), Project.find(2)]) + assert_equal 2, aredridel.projects.size + assert_equal 2, aredridel.projects(true).size + end + + def test_adding_uses_default_values_on_join_table + ac = projects(:action_controller) + assert !developers(:jamis).projects.include?(ac) + developers(:jamis).projects << ac + + assert developers(:jamis, :reload).projects.include?(ac) + project = developers(:jamis).projects.detect { |p| p == ac } + assert_equal 1, project.access_level.to_i + end + + def test_adding_uses_explicit_values_on_join_table + ac = projects(:action_controller) + assert !developers(:jamis).projects.include?(ac) + developers(:jamis).projects.push_with_attributes(ac, :access_level => 3) + + assert developers(:jamis, :reload).projects.include?(ac) + project = developers(:jamis).projects.detect { |p| p == ac } + assert_equal 3, project.access_level.to_i + end + + def test_hatbm_attribute_access_and_respond_to + project = developers(:jamis).projects[0] + assert project.has_attribute?("name") + assert project.has_attribute?("joined_on") + assert project.has_attribute?("access_level") + assert project.respond_to?("name") + assert project.respond_to?("name=") + assert project.respond_to?("name?") + assert project.respond_to?("joined_on") + assert project.respond_to?("joined_on=") + assert project.respond_to?("joined_on?") + assert project.respond_to?("access_level") + assert project.respond_to?("access_level=") + assert project.respond_to?("access_level?") + end + + def test_habtm_adding_before_save + no_of_devels = Developer.count + no_of_projects = Project.count + aredridel = Developer.new("name" => "Aredridel") + aredridel.projects.concat([Project.find(1), p = Project.new("name" => "Projekt")]) + assert aredridel.new_record? + assert p.new_record? + assert aredridel.save + assert !aredridel.new_record? + assert_equal no_of_devels+1, Developer.count + assert_equal no_of_projects+1, Project.count + assert_equal 2, aredridel.projects.size + assert_equal 2, aredridel.projects(true).size + end + + def test_habtm_adding_before_save_with_join_attributes + no_of_devels = Developer.count + no_of_projects = Project.count + now = Date.today + ken = Developer.new("name" => "Ken") + ken.projects.push_with_attributes( Project.find(1), :joined_on => now ) + p = Project.new("name" => "Foomatic") + ken.projects.push_with_attributes( p, :joined_on => now ) + assert ken.new_record? + assert p.new_record? + assert ken.save + assert !ken.new_record? + assert_equal no_of_devels+1, Developer.count + assert_equal no_of_projects+1, Project.count + assert_equal 2, ken.projects.size + assert_equal 2, ken.projects(true).size + + kenReloaded = Developer.find_by_name 'Ken' + kenReloaded.projects.each {|prj| assert_date_from_db(now, prj.joined_on)} + end + + def test_habtm_saving_multiple_relationships + new_project = Project.new("name" => "Grimetime") + amount_of_developers = 4 + developers = (0..amount_of_developers).collect {|i| Developer.create(:name => "JME #{i}") } + + new_project.developer_ids = [developers[0].id, developers[1].id] + new_project.developers_with_callback_ids = [developers[2].id, developers[3].id] + assert new_project.save + + new_project.reload + assert_equal amount_of_developers, new_project.developers.size + amount_of_developers.times do |i| + assert_equal developers[i].name, new_project.developers[i].name + end + end + + def test_build + devel = Developer.find(1) + proj = devel.projects.build("name" => "Projekt") + assert_equal devel.projects.last, proj + assert proj.new_record? + devel.save + assert !proj.new_record? + assert_equal devel.projects.last, proj + end + + def test_create + devel = Developer.find(1) + proj = devel.projects.create("name" => "Projekt") + assert_equal devel.projects.last, proj + assert !proj.new_record? + end + + def test_uniq_after_the_fact + developers(:jamis).projects << projects(:active_record) + developers(:jamis).projects << projects(:active_record) + assert_equal 3, developers(:jamis).projects.size + assert_equal 1, developers(:jamis).projects.uniq.size + end + + def test_uniq_before_the_fact + projects(:active_record).developers << developers(:jamis) + projects(:active_record).developers << developers(:david) + assert_equal 3, projects(:active_record, :reload).developers.size + end + + def test_deleting + david = Developer.find(1) + active_record = Project.find(1) + david.projects.reload + assert_equal 2, david.projects.size + assert_equal 3, active_record.developers.size + + david.projects.delete(active_record) + + assert_equal 1, david.projects.size + assert_equal 1, david.projects(true).size + assert_equal 2, active_record.developers(true).size + end + + def test_deleting_array + david = Developer.find(1) + david.projects.reload + david.projects.delete(Project.find(:all)) + assert_equal 0, david.projects.size + assert_equal 0, david.projects(true).size + end + + def test_deleting_with_sql + david = Developer.find(1) + active_record = Project.find(1) + active_record.developers.reload + assert_equal 3, active_record.developers_by_sql.size + + active_record.developers_by_sql.delete(david) + assert_equal 2, active_record.developers_by_sql(true).size + end + + def test_deleting_array_with_sql + active_record = Project.find(1) + active_record.developers.reload + assert_equal 3, active_record.developers_by_sql.size + + active_record.developers_by_sql.delete(Developer.find(:all)) + assert_equal 0, active_record.developers_by_sql(true).size + end + + def test_deleting_all + david = Developer.find(1) + david.projects.reload + david.projects.clear + assert_equal 0, david.projects.size + assert_equal 0, david.projects(true).size + end + + def test_removing_associations_on_destroy + david = DeveloperWithBeforeDestroyRaise.find(1) + assert !david.projects.empty? + assert_nothing_raised { david.destroy } + assert david.projects.empty? + assert DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1").empty? + end + + def test_additional_columns_from_join_table + assert_date_from_db Date.new(2004, 10, 10), Developer.find(1).projects.first.joined_on + end + + def test_destroy_all + david = Developer.find(1) + david.projects.reload + assert !david.projects.empty? + david.projects.destroy_all + assert david.projects.empty? + assert david.projects(true).empty? + end + + def test_rich_association + jamis = developers(:jamis) + jamis.projects.push_with_attributes(projects(:action_controller), :joined_on => Date.today) + + assert_date_from_db Date.today, jamis.projects.select { |p| p.name == projects(:action_controller).name }.first.joined_on + assert_date_from_db Date.today, developers(:jamis).projects.select { |p| p.name == projects(:action_controller).name }.first.joined_on + end + + def test_associations_with_conditions + assert_equal 3, projects(:active_record).developers.size + assert_equal 1, projects(:active_record).developers_named_david.size + + assert_equal developers(:david), projects(:active_record).developers_named_david.find(developers(:david).id) + assert_equal developers(:david), projects(:active_record).salaried_developers.find(developers(:david).id) + + projects(:active_record).developers_named_david.clear + assert_equal 2, projects(:active_record, :reload).developers.size + end + + def test_find_in_association + # Using sql + assert_equal developers(:david), projects(:active_record).developers.find(developers(:david).id), "SQL find" + + # Using ruby + active_record = projects(:active_record) + active_record.developers.reload + assert_equal developers(:david), active_record.developers.find(developers(:david).id), "Ruby find" + end + + def test_find_in_association_with_custom_finder_sql + assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id), "SQL find" + + active_record = projects(:active_record) + active_record.developers_with_finder_sql.reload + assert_equal developers(:david), active_record.developers_with_finder_sql.find(developers(:david).id), "Ruby find" + end + + def test_find_in_association_with_custom_finder_sql_and_string_id + assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id.to_s), "SQL find" + end + + def test_find_with_merged_options + assert_equal 1, projects(:active_record).limited_developers.size + assert_equal 1, projects(:active_record).limited_developers.find(:all).size + assert_equal 3, projects(:active_record).limited_developers.find(:all, :limit => nil).size + end + + def test_new_with_values_in_collection + jamis = DeveloperForProjectWithAfterCreateHook.find_by_name('Jamis') + david = DeveloperForProjectWithAfterCreateHook.find_by_name('David') + project = ProjectWithAfterCreateHook.new(:name => "Cooking with Bertie") + project.developers << jamis + project.save! + project.reload + + assert project.developers.include?(jamis) + assert project.developers.include?(david) + end + + def test_find_in_association_with_options + developers = projects(:active_record).developers.find(:all) + assert_equal 3, developers.size + + assert_equal developers(:poor_jamis), projects(:active_record).developers.find(:first, :conditions => "salary < 10000") + assert_equal developers(:jamis), projects(:active_record).developers.find(:first, :order => "salary DESC") + end + + def test_replace_with_less + david = developers(:david) + david.projects = [projects(:action_controller)] + assert david.save + assert_equal 1, david.projects.length + end + + def test_replace_with_new + david = developers(:david) + david.projects = [projects(:action_controller), Project.new("name" => "ActionWebSearch")] + david.save + assert_equal 2, david.projects.length + assert !david.projects.include?(projects(:active_record)) + end + + def test_replace_on_new_object + new_developer = Developer.new("name" => "Matz") + new_developer.projects = [projects(:action_controller), Project.new("name" => "ActionWebSearch")] + new_developer.save + assert_equal 2, new_developer.projects.length + end + + def test_consider_type + developer = Developer.find(:first) + special_project = SpecialProject.create("name" => "Special Project") + + other_project = developer.projects.first + developer.special_projects << special_project + developer.reload + + assert developer.projects.include?(special_project) + assert developer.special_projects.include?(special_project) + assert !developer.special_projects.include?(other_project) + end + + def test_update_attributes_after_push_without_duplicate_join_table_rows + developer = Developer.new("name" => "Kano") + project = SpecialProject.create("name" => "Special Project") + assert developer.save + developer.projects << project + developer.update_attribute("name", "Bruza") + assert_equal 1, Developer.connection.select_value(<<-end_sql).to_i + SELECT count(*) FROM developers_projects + WHERE project_id = #{project.id} + AND developer_id = #{developer.id} + end_sql + end + + def test_updating_attributes_on_non_rich_associations + welcome = categories(:technology).posts.first + welcome.title = "Something else" + assert welcome.save! + end + + def test_updating_attributes_on_rich_associations + david = projects(:action_controller).developers.first + david.name = "DHH" + assert_raises(ActiveRecord::ReadOnlyRecord) { david.save! } + end + + + def test_updating_attributes_on_rich_associations_with_limited_find + david = projects(:action_controller).developers.find(:all, :select => "developers.*").first + david.name = "DHH" + assert david.save! + end + + def test_join_table_alias + assert_equal 3, Developer.find(:all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL').size + end +end diff --git a/vendor/rails/activerecord/test/base_test.rb b/vendor/rails/activerecord/test/base_test.rb new file mode 100755 index 00000000..f5ad7abc --- /dev/null +++ b/vendor/rails/activerecord/test/base_test.rb @@ -0,0 +1,1314 @@ +require 'abstract_unit' +require 'fixtures/topic' +require 'fixtures/reply' +require 'fixtures/company' +require 'fixtures/customer' +require 'fixtures/developer' +require 'fixtures/project' +require 'fixtures/default' +require 'fixtures/auto_id' +require 'fixtures/column_name' +require 'fixtures/subscriber' +require 'fixtures/keyboard' + +class Category < ActiveRecord::Base; end +class Smarts < ActiveRecord::Base; end +class CreditCard < ActiveRecord::Base; end +class MasterCreditCard < ActiveRecord::Base; end +class Post < ActiveRecord::Base; end +class Computer < ActiveRecord::Base; end +class NonExistentTable < ActiveRecord::Base; end +class TestOracleDefault < ActiveRecord::Base; end + +class LoosePerson < ActiveRecord::Base + attr_protected :credit_rating, :administrator + self.abstract_class = true +end + +class LooseDescendant < LoosePerson + attr_protected :phone_number +end + +class TightPerson < ActiveRecord::Base + attr_accessible :name, :address +end + +class TightDescendant < TightPerson + attr_accessible :phone_number +end + +class Booleantest < ActiveRecord::Base; end + +class Task < ActiveRecord::Base + attr_protected :starting +end + +class BasicsTest < Test::Unit::TestCase + fixtures :topics, :companies, :developers, :projects, :computers + + def test_table_exists + assert !NonExistentTable.table_exists? + assert Topic.table_exists? + end + + def test_set_attributes + topic = Topic.find(1) + topic.attributes = { "title" => "Budget", "author_name" => "Jason" } + topic.save + assert_equal("Budget", topic.title) + assert_equal("Jason", topic.author_name) + assert_equal(topics(:first).author_email_address, Topic.find(1).author_email_address) + end + + def test_integers_as_nil + test = AutoId.create('value' => '') + assert_nil AutoId.find(test.id).value + end + + def test_set_attributes_with_block + topic = Topic.new do |t| + t.title = "Budget" + t.author_name = "Jason" + end + + assert_equal("Budget", topic.title) + assert_equal("Jason", topic.author_name) + end + + def test_respond_to? + topic = Topic.find(1) + assert topic.respond_to?("title") + assert topic.respond_to?("title?") + assert topic.respond_to?("title=") + assert topic.respond_to?(:title) + assert topic.respond_to?(:title?) + assert topic.respond_to?(:title=) + assert topic.respond_to?("author_name") + assert topic.respond_to?("attribute_names") + assert !topic.respond_to?("nothingness") + assert !topic.respond_to?(:nothingness) + end + + def test_array_content + topic = Topic.new + topic.content = %w( one two three ) + topic.save + + assert_equal(%w( one two three ), Topic.find(topic.id).content) + end + + def test_hash_content + topic = Topic.new + topic.content = { "one" => 1, "two" => 2 } + topic.save + + assert_equal 2, Topic.find(topic.id).content["two"] + + topic.content["three"] = 3 + topic.save + + assert_equal 3, Topic.find(topic.id).content["three"] + end + + def test_update_array_content + topic = Topic.new + topic.content = %w( one two three ) + + topic.content.push "four" + assert_equal(%w( one two three four ), topic.content) + + topic.save + + topic = Topic.find(topic.id) + topic.content << "five" + assert_equal(%w( one two three four five ), topic.content) + end + + def test_case_sensitive_attributes_hash + # DB2 is not case-sensitive + return true if current_adapter?(:DB2Adapter) + + assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.find(:first).attributes + end + + def test_create + topic = Topic.new + topic.title = "New Topic" + topic.save + topic_reloaded = Topic.find(topic.id) + assert_equal("New Topic", topic_reloaded.title) + end + + def test_save! + topic = Topic.new(:title => "New Topic") + assert topic.save! + end + + def test_hashes_not_mangled + new_topic = { :title => "New Topic" } + new_topic_values = { :title => "AnotherTopic" } + + topic = Topic.new(new_topic) + assert_equal new_topic[:title], topic.title + + topic.attributes= new_topic_values + assert_equal new_topic_values[:title], topic.title + end + + def test_create_many + topics = Topic.create([ { "title" => "first" }, { "title" => "second" }]) + assert_equal 2, topics.size + assert_equal "first", topics.first.title + end + + def test_create_columns_not_equal_attributes + topic = Topic.new + topic.title = 'Another New Topic' + topic.send :write_attribute, 'does_not_exist', 'test' + assert_nothing_raised { topic.save } + end + + def test_create_through_factory + topic = Topic.create("title" => "New Topic") + topicReloaded = Topic.find(topic.id) + assert_equal(topic, topicReloaded) + end + + def test_update + topic = Topic.new + topic.title = "Another New Topic" + topic.written_on = "2003-12-12 23:23:00" + topic.save + topicReloaded = Topic.find(topic.id) + assert_equal("Another New Topic", topicReloaded.title) + + topicReloaded.title = "Updated topic" + topicReloaded.save + + topicReloadedAgain = Topic.find(topic.id) + + assert_equal("Updated topic", topicReloadedAgain.title) + end + + def test_update_columns_not_equal_attributes + topic = Topic.new + topic.title = "Still another topic" + topic.save + + topicReloaded = Topic.find(topic.id) + topicReloaded.title = "A New Topic" + topicReloaded.send :write_attribute, 'does_not_exist', 'test' + assert_nothing_raised { topicReloaded.save } + end + + def test_write_attribute + topic = Topic.new + topic.send(:write_attribute, :title, "Still another topic") + assert_equal "Still another topic", topic.title + + topic.send(:write_attribute, "title", "Still another topic: part 2") + assert_equal "Still another topic: part 2", topic.title + end + + def test_read_attribute + topic = Topic.new + topic.title = "Don't change the topic" + assert_equal "Don't change the topic", topic.send(:read_attribute, "title") + assert_equal "Don't change the topic", topic["title"] + + assert_equal "Don't change the topic", topic.send(:read_attribute, :title) + assert_equal "Don't change the topic", topic[:title] + end + + def test_read_attribute_when_false + topic = topics(:first) + topic.approved = false + assert !topic.approved?, "approved should be false" + topic.approved = "false" + assert !topic.approved?, "approved should be false" + end + + def test_read_attribute_when_true + topic = topics(:first) + topic.approved = true + assert topic.approved?, "approved should be true" + topic.approved = "true" + assert topic.approved?, "approved should be true" + end + + def test_read_write_boolean_attribute + topic = Topic.new + # puts "" + # puts "New Topic" + # puts topic.inspect + topic.approved = "false" + # puts "Expecting false" + # puts topic.inspect + assert !topic.approved?, "approved should be false" + topic.approved = "false" + # puts "Expecting false" + # puts topic.inspect + assert !topic.approved?, "approved should be false" + topic.approved = "true" + # puts "Expecting true" + # puts topic.inspect + assert topic.approved?, "approved should be true" + topic.approved = "true" + # puts "Expecting true" + # puts topic.inspect + assert topic.approved?, "approved should be true" + # puts "" + end + + def test_reader_generation + Topic.find(:first).title + Firm.find(:first).name + Client.find(:first).name + if ActiveRecord::Base.generate_read_methods + assert_readers(Topic, %w(type replies_count)) + assert_readers(Firm, %w(type)) + assert_readers(Client, %w(type ruby_type rating?)) + else + [Topic, Firm, Client].each {|klass| assert_equal klass.read_methods, {}} + end + end + + def test_reader_for_invalid_column_names + # column names which aren't legal ruby ids + topic = Topic.find(:first) + topic.send(:define_read_method, "mumub-jumbo".to_sym, "mumub-jumbo", nil) + assert !Topic.read_methods.include?("mumub-jumbo") + end + + def test_non_attribute_access_and_assignment + topic = Topic.new + assert !topic.respond_to?("mumbo") + assert_raises(NoMethodError) { topic.mumbo } + assert_raises(NoMethodError) { topic.mumbo = 5 } + end + + def test_preserving_date_objects + # SQL Server doesn't have a separate column type just for dates, so all are returned as time + return true if current_adapter?(:SQLServerAdapter) + + if current_adapter?(:SybaseAdapter) + # Sybase ctlib does not (yet?) support the date type; use datetime instead. + assert_kind_of( + Time, Topic.find(1).last_read, + "The last_read attribute should be of the Time class" + ) + else + assert_kind_of( + Date, Topic.find(1).last_read, + "The last_read attribute should be of the Date class" + ) + end + end + + def test_preserving_time_objects + assert_kind_of( + Time, Topic.find(1).bonus_time, + "The bonus_time attribute should be of the Time class" + ) + + assert_kind_of( + Time, Topic.find(1).written_on, + "The written_on attribute should be of the Time class" + ) + end + + def test_destroy + topic = Topic.new + topic.title = "Yet Another New Topic" + topic.written_on = "2003-12-12 23:23:00" + topic.save + topic.destroy + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) } + end + + def test_destroy_returns_self + topic = Topic.new("title" => "Yet Another Title") + assert topic.save + assert_equal topic, topic.destroy, "destroy did not return destroyed object" + end + + def test_record_not_found_exception + assert_raises(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(99999) } + end + + def test_initialize_with_attributes + topic = Topic.new({ + "title" => "initialized from attributes", "written_on" => "2003-12-12 23:23" + }) + + assert_equal("initialized from attributes", topic.title) + end + + def test_initialize_with_invalid_attribute + begin + topic = Topic.new({ "title" => "test", + "last_read(1i)" => "2005", "last_read(2i)" => "2", "last_read(3i)" => "31"}) + rescue ActiveRecord::MultiparameterAssignmentErrors => ex + assert_equal(1, ex.errors.size) + assert_equal("last_read", ex.errors[0].attribute) + end + end + + def test_load + topics = Topic.find(:all, :order => 'id') + assert_equal(2, topics.size) + assert_equal(topics(:first).title, topics.first.title) + end + + def test_load_with_condition + topics = Topic.find(:all, :conditions => "author_name = 'Mary'") + + assert_equal(1, topics.size) + assert_equal(topics(:second).title, topics.first.title) + end + + def test_table_name_guesses + assert_equal "topics", Topic.table_name + + assert_equal "categories", Category.table_name + assert_equal "smarts", Smarts.table_name + assert_equal "credit_cards", CreditCard.table_name + assert_equal "master_credit_cards", MasterCreditCard.table_name + + ActiveRecord::Base.pluralize_table_names = false + [Category, Smarts, CreditCard, MasterCreditCard].each{|c| c.reset_table_name} + assert_equal "category", Category.table_name + assert_equal "smarts", Smarts.table_name + assert_equal "credit_card", CreditCard.table_name + assert_equal "master_credit_card", MasterCreditCard.table_name + ActiveRecord::Base.pluralize_table_names = true + [Category, Smarts, CreditCard, MasterCreditCard].each{|c| c.reset_table_name} + + ActiveRecord::Base.table_name_prefix = "test_" + Category.reset_table_name + assert_equal "test_categories", Category.table_name + ActiveRecord::Base.table_name_suffix = "_test" + Category.reset_table_name + assert_equal "test_categories_test", Category.table_name + ActiveRecord::Base.table_name_prefix = "" + Category.reset_table_name + assert_equal "categories_test", Category.table_name + ActiveRecord::Base.table_name_suffix = "" + Category.reset_table_name + assert_equal "categories", Category.table_name + + ActiveRecord::Base.pluralize_table_names = false + ActiveRecord::Base.table_name_prefix = "test_" + Category.reset_table_name + assert_equal "test_category", Category.table_name + ActiveRecord::Base.table_name_suffix = "_test" + Category.reset_table_name + assert_equal "test_category_test", Category.table_name + ActiveRecord::Base.table_name_prefix = "" + Category.reset_table_name + assert_equal "category_test", Category.table_name + ActiveRecord::Base.table_name_suffix = "" + Category.reset_table_name + assert_equal "category", Category.table_name + ActiveRecord::Base.pluralize_table_names = true + [Category, Smarts, CreditCard, MasterCreditCard].each{|c| c.reset_table_name} + end + + def test_destroy_all + assert_equal 2, Topic.count + + Topic.destroy_all "author_name = 'Mary'" + assert_equal 1, Topic.count + end + + def test_destroy_many + assert_equal 3, Client.count + Client.destroy([2, 3]) + assert_equal 1, Client.count + end + + def test_delete_many + Topic.delete([1, 2]) + assert_equal 0, Topic.count + end + + def test_boolean_attributes + assert ! Topic.find(1).approved? + assert Topic.find(2).approved? + end + + def test_increment_counter + Topic.increment_counter("replies_count", 1) + assert_equal 1, Topic.find(1).replies_count + + Topic.increment_counter("replies_count", 1) + assert_equal 2, Topic.find(1).replies_count + end + + def test_decrement_counter + Topic.decrement_counter("replies_count", 2) + assert_equal 1, Topic.find(2).replies_count + + Topic.decrement_counter("replies_count", 2) + assert_equal 0, Topic.find(1).replies_count + end + + def test_update_all + # The ADO library doesn't support the number of affected rows + return true if current_adapter?(:SQLServerAdapter) + + assert_equal 2, Topic.update_all("content = 'bulk updated!'") + assert_equal "bulk updated!", Topic.find(1).content + assert_equal "bulk updated!", Topic.find(2).content + assert_equal 2, Topic.update_all(['content = ?', 'bulk updated again!']) + assert_equal "bulk updated again!", Topic.find(1).content + assert_equal "bulk updated again!", Topic.find(2).content + end + + def test_update_many + topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } } + updated = Topic.update(topic_data.keys, topic_data.values) + + assert_equal 2, updated.size + assert_equal "1 updated", Topic.find(1).content + assert_equal "2 updated", Topic.find(2).content + end + + def test_delete_all + # The ADO library doesn't support the number of affected rows + return true if current_adapter?(:SQLServerAdapter) + + assert_equal 2, Topic.delete_all + end + + def test_update_by_condition + Topic.update_all "content = 'bulk updated!'", ["approved = ?", true] + assert_equal "Have a nice day", Topic.find(1).content + assert_equal "bulk updated!", Topic.find(2).content + end + + def test_attribute_present + t = Topic.new + t.title = "hello there!" + t.written_on = Time.now + assert t.attribute_present?("title") + assert t.attribute_present?("written_on") + assert !t.attribute_present?("content") + end + + def test_attribute_keys_on_new_instance + t = Topic.new + assert_equal nil, t.title, "The topics table has a title column, so it should be nil" + assert_raise(NoMethodError) { t.title2 } + end + + def test_class_name + assert_equal "Firm", ActiveRecord::Base.class_name("firms") + assert_equal "Category", ActiveRecord::Base.class_name("categories") + assert_equal "AccountHolder", ActiveRecord::Base.class_name("account_holder") + + ActiveRecord::Base.pluralize_table_names = false + assert_equal "Firms", ActiveRecord::Base.class_name( "firms" ) + ActiveRecord::Base.pluralize_table_names = true + + ActiveRecord::Base.table_name_prefix = "test_" + assert_equal "Firm", ActiveRecord::Base.class_name( "test_firms" ) + ActiveRecord::Base.table_name_suffix = "_tests" + assert_equal "Firm", ActiveRecord::Base.class_name( "test_firms_tests" ) + ActiveRecord::Base.table_name_prefix = "" + assert_equal "Firm", ActiveRecord::Base.class_name( "firms_tests" ) + ActiveRecord::Base.table_name_suffix = "" + assert_equal "Firm", ActiveRecord::Base.class_name( "firms" ) + end + + def test_null_fields + assert_nil Topic.find(1).parent_id + assert_nil Topic.create("title" => "Hey you").parent_id + end + + def test_default_values + topic = Topic.new + assert topic.approved? + assert_nil topic.written_on + assert_nil topic.bonus_time + assert_nil topic.last_read + + topic.save + + topic = Topic.find(topic.id) + assert topic.approved? + assert_nil topic.last_read + + # Oracle has some funky default handling, so it requires a bit of + # extra testing. See ticket #2788. + if current_adapter?(:OracleAdapter) + test = TestOracleDefault.new + assert_equal "X", test.test_char + assert_equal "hello", test.test_string + assert_equal 3, test.test_int + end + end + + def test_utc_as_time_zone + # Oracle and SQLServer do not have a TIME datatype. + return true if current_adapter?(:SQLServerAdapter) || current_adapter?(:OracleAdapter) + + Topic.default_timezone = :utc + attributes = { "bonus_time" => "5:42:00AM" } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time + Topic.default_timezone = :local + end + + def test_default_values_on_empty_strings + topic = Topic.new + topic.approved = nil + topic.last_read = nil + + topic.save + + topic = Topic.find(topic.id) + assert_nil topic.last_read + + # Sybase adapter does not allow nulls in boolean columns + if current_adapter?(:SybaseAdapter) + assert topic.approved == false + else + assert_nil topic.approved + end + end + + def test_equality + assert_equal Topic.find(1), Topic.find(2).topic + end + + def test_equality_of_new_records + assert_not_equal Topic.new, Topic.new + end + + def test_hashing + assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ] + end + + def test_destroy_new_record + client = Client.new + client.destroy + assert client.frozen? + end + + def test_destroy_record_with_associations + client = Client.find(3) + client.destroy + assert client.frozen? + assert_kind_of Firm, client.firm + assert_raises(TypeError) { client.name = "something else" } + end + + def test_update_attribute + assert !Topic.find(1).approved? + Topic.find(1).update_attribute("approved", true) + assert Topic.find(1).approved? + + Topic.find(1).update_attribute(:approved, false) + assert !Topic.find(1).approved? + end + + def test_mass_assignment_protection + firm = Firm.new + firm.attributes = { "name" => "Next Angle", "rating" => 5 } + assert_equal 1, firm.rating + end + + def test_customized_primary_key_remains_protected + subscriber = Subscriber.new(:nick => 'webster123', :name => 'nice try') + assert_nil subscriber.id + + keyboard = Keyboard.new(:key_number => 9, :name => 'nice try') + assert_nil keyboard.id + end + + def test_customized_primary_key_remains_protected_when_refered_to_as_id + subscriber = Subscriber.new(:id => 'webster123', :name => 'nice try') + assert_nil subscriber.id + + keyboard = Keyboard.new(:id => 9, :name => 'nice try') + assert_nil keyboard.id + end + + def test_mass_assignment_protection_on_defaults + firm = Firm.new + firm.attributes = { "id" => 5, "type" => "Client" } + assert_nil firm.id + assert_equal "Firm", firm[:type] + end + + def test_mass_assignment_accessible + reply = Reply.new("title" => "hello", "content" => "world", "approved" => true) + reply.save + + assert reply.approved? + + reply.approved = false + reply.save + + assert !reply.approved? + end + + def test_mass_assignment_protection_inheritance + assert_nil LoosePerson.accessible_attributes + assert_equal [ :credit_rating, :administrator ], LoosePerson.protected_attributes + + assert_nil LooseDescendant.accessible_attributes + assert_equal [ :credit_rating, :administrator, :phone_number ], LooseDescendant.protected_attributes + + assert_nil TightPerson.protected_attributes + assert_equal [ :name, :address ], TightPerson.accessible_attributes + + assert_nil TightDescendant.protected_attributes + assert_equal [ :name, :address, :phone_number ], TightDescendant.accessible_attributes + end + + def test_multiparameter_attributes_on_date + attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" } + topic = Topic.find(1) + topic.attributes = attributes + # note that extra #to_date call allows test to pass for Oracle, which + # treats dates/times the same + assert_date_from_db Date.new(2004, 6, 24), topic.last_read.to_date + end + + def test_multiparameter_attributes_on_date_with_empty_date + attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "" } + topic = Topic.find(1) + topic.attributes = attributes + # note that extra #to_date call allows test to pass for Oracle, which + # treats dates/times the same + assert_date_from_db Date.new(2004, 6, 1), topic.last_read.to_date + end + + def test_multiparameter_attributes_on_date_with_all_empty + attributes = { "last_read(1i)" => "", "last_read(2i)" => "", "last_read(3i)" => "" } + topic = Topic.find(1) + topic.attributes = attributes + assert_nil topic.last_read + end + + def test_multiparameter_attributes_on_time + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on + end + + def test_multiparameter_attributes_on_time_with_empty_seconds + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on + end + + def test_multiparameter_mass_assignment_protector + task = Task.new + time = Time.mktime(2000, 1, 1, 1) + task.starting = time + attributes = { "starting(1i)" => "2004", "starting(2i)" => "6", "starting(3i)" => "24" } + task.attributes = attributes + assert_equal time, task.starting + end + + def test_multiparameter_assignment_of_aggregation + customer = Customer.new + address = Address.new("The Street", "The City", "The Country") + attributes = { "address(1)" => address.street, "address(2)" => address.city, "address(3)" => address.country } + customer.attributes = attributes + assert_equal address, customer.address + end + + def test_attributes_on_dummy_time + # Oracle and SQL Server do not have a TIME datatype. + return true if current_adapter?(:SQLServerAdapter) || current_adapter?(:OracleAdapter) + + attributes = { + "bonus_time" => "5:42:00AM" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time + end + + def test_boolean + b_false = Booleantest.create({ "value" => false }) + false_id = b_false.id + b_true = Booleantest.create({ "value" => true }) + true_id = b_true.id + + b_false = Booleantest.find(false_id) + assert !b_false.value? + b_true = Booleantest.find(true_id) + assert b_true.value? + end + + def test_boolean_cast_from_string + b_false = Booleantest.create({ "value" => "0" }) + false_id = b_false.id + b_true = Booleantest.create({ "value" => "1" }) + true_id = b_true.id + + b_false = Booleantest.find(false_id) + assert !b_false.value? + b_true = Booleantest.find(true_id) + assert b_true.value? + end + + def test_clone + topic = Topic.find(1) + cloned_topic = nil + assert_nothing_raised { cloned_topic = topic.clone } + assert_equal topic.title, cloned_topic.title + assert cloned_topic.new_record? + + # test if the attributes have been cloned + topic.title = "a" + cloned_topic.title = "b" + assert_equal "a", topic.title + assert_equal "b", cloned_topic.title + + # test if the attribute values have been cloned + topic.title = {"a" => "b"} + cloned_topic = topic.clone + cloned_topic.title["a"] = "c" + assert_equal "b", topic.title["a"] + + cloned_topic.save + assert !cloned_topic.new_record? + assert cloned_topic.id != topic.id + end + + def test_clone_with_aggregate_of_same_name_as_attribute + dev = DeveloperWithAggregate.find(1) + assert_kind_of DeveloperSalary, dev.salary + + clone = nil + assert_nothing_raised { clone = dev.clone } + assert_kind_of DeveloperSalary, clone.salary + assert_equal dev.salary.amount, clone.salary.amount + assert clone.new_record? + + # test if the attributes have been cloned + original_amount = clone.salary.amount + dev.salary.amount = 1 + assert_equal original_amount, clone.salary.amount + + assert clone.save + assert !clone.new_record? + assert clone.id != dev.id + end + + def test_clone_preserves_subtype + clone = nil + assert_nothing_raised { clone = Company.find(3).clone } + assert_kind_of Client, clone + end + + def test_bignum + company = Company.find(1) + company.rating = 2147483647 + company.save + company = Company.find(1) + assert_equal 2147483647, company.rating + end + + # TODO: extend defaults tests to other databases! + if current_adapter?(:PostgreSQLAdapter) + def test_default + default = Default.new + + # fixed dates / times + assert_equal Date.new(2004, 1, 1), default.fixed_date + assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time + + # char types + assert_equal 'Y', default.char1 + assert_equal 'a varchar field', default.char2 + assert_equal 'a text field', default.char3 + end + + class Geometric < ActiveRecord::Base; end + def test_geometric_content + + # accepted format notes: + # ()'s aren't required + # values can be a mix of float or integer + + g = Geometric.new( + :a_point => '(5.0, 6.1)', + #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql + :a_line_segment => '(2.0, 3), (5.5, 7.0)', + :a_box => '2.0, 3, 5.5, 7.0', + :a_path => '[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]', # [ ] is an open path + :a_polygon => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', + :a_circle => '<(5.3, 10.4), 2>' + ) + + assert g.save + + # Reload and check that we have all the geometric attributes. + h = Geometric.find(g.id) + + assert_equal '(5,6.1)', h.a_point + assert_equal '[(2,3),(5.5,7)]', h.a_line_segment + assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner + assert_equal '[(2,3),(5.5,7),(8.5,11)]', h.a_path + assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon + assert_equal '<(5.3,10.4),2>', h.a_circle + + # use a geometric function to test for an open path + objs = Geometric.find_by_sql ["select isopen(a_path) from geometrics where id = ?", g.id] + assert_equal objs[0].isopen, 't' + + # test alternate formats when defining the geometric types + + g = Geometric.new( + :a_point => '5.0, 6.1', + #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql + :a_line_segment => '((2.0, 3), (5.5, 7.0))', + :a_box => '(2.0, 3), (5.5, 7.0)', + :a_path => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', # ( ) is a closed path + :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0', + :a_circle => '((5.3, 10.4), 2)' + ) + + assert g.save + + # Reload and check that we have all the geometric attributes. + h = Geometric.find(g.id) + + assert_equal '(5,6.1)', h.a_point + assert_equal '[(2,3),(5.5,7)]', h.a_line_segment + assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner + assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path + assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon + assert_equal '<(5.3,10.4),2>', h.a_circle + + # use a geometric function to test for an closed path + objs = Geometric.find_by_sql ["select isclosed(a_path) from geometrics where id = ?", g.id] + assert_equal objs[0].isclosed, 't' + end + end + + def test_auto_id + auto = AutoId.new + auto.save + assert (auto.id > 0) + end + + def quote_column_name(name) + "<#{name}>" + end + + def test_quote_keys + ar = AutoId.new + source = {"foo" => "bar", "baz" => "quux"} + actual = ar.send(:quote_columns, self, source) + inverted = actual.invert + assert_equal("", inverted["bar"]) + assert_equal("", inverted["quux"]) + end + + def test_sql_injection_via_find + assert_raises(ActiveRecord::RecordNotFound) do + Topic.find("123456 OR id > 0") + end + + assert_raises(ActiveRecord::RecordNotFound) do + Topic.find(";;; this should raise an RecordNotFound error") + end + end + + def test_column_name_properly_quoted + col_record = ColumnName.new + col_record.references = 40 + assert col_record.save + col_record.references = 41 + assert col_record.save + assert_not_nil c2 = ColumnName.find(col_record.id) + assert_equal(41, c2.references) + end + + MyObject = Struct.new :attribute1, :attribute2 + + def test_serialized_attribute + myobj = MyObject.new('value1', 'value2') + topic = Topic.create("content" => myobj) + Topic.serialize("content", MyObject) + assert_equal(myobj, topic.content) + end + + def test_serialized_attribute_with_class_constraint + myobj = MyObject.new('value1', 'value2') + topic = Topic.create("content" => myobj) + Topic.serialize(:content, Hash) + + assert_raise(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).content } + + settings = { "color" => "blue" } + Topic.find(topic.id).update_attribute("content", settings) + assert_equal(settings, Topic.find(topic.id).content) + Topic.serialize(:content) + end + + def test_quote + author_name = "\\ \001 ' \n \\n \"" + topic = Topic.create('author_name' => author_name) + assert_equal author_name, Topic.find(topic.id).author_name + end + + def test_class_level_destroy + should_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world") + Topic.find(1).replies << should_be_destroyed_reply + + Topic.destroy(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) } + assert_raise(ActiveRecord::RecordNotFound) { Reply.find(should_be_destroyed_reply.id) } + end + + def test_class_level_delete + should_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world") + Topic.find(1).replies << should_be_destroyed_reply + + Topic.delete(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) } + assert_nothing_raised { Reply.find(should_be_destroyed_reply.id) } + end + + def test_increment_attribute + assert_equal 0, topics(:first).replies_count + topics(:first).increment! :replies_count + assert_equal 1, topics(:first, :reload).replies_count + + topics(:first).increment(:replies_count).increment!(:replies_count) + assert_equal 3, topics(:first, :reload).replies_count + end + + def test_increment_nil_attribute + assert_nil topics(:first).parent_id + topics(:first).increment! :parent_id + assert_equal 1, topics(:first).parent_id + end + + def test_decrement_attribute + topics(:first).increment(:replies_count).increment!(:replies_count) + assert_equal 2, topics(:first).replies_count + + topics(:first).decrement!(:replies_count) + assert_equal 1, topics(:first, :reload).replies_count + + topics(:first).decrement(:replies_count).decrement!(:replies_count) + assert_equal -1, topics(:first, :reload).replies_count + end + + def test_toggle_attribute + assert !topics(:first).approved? + topics(:first).toggle!(:approved) + assert topics(:first).approved? + topic = topics(:first) + topic.toggle(:approved) + assert !topic.approved? + topic.reload + assert topic.approved? + end + + def test_reload + t1 = Topic.find(1) + t2 = Topic.find(1) + t1.title = "something else" + t1.save + t2.reload + assert_equal t1.title, t2.title + end + + def test_define_attr_method_with_value + k = Class.new( ActiveRecord::Base ) + k.send(:define_attr_method, :table_name, "foo") + assert_equal "foo", k.table_name + end + + def test_define_attr_method_with_block + k = Class.new( ActiveRecord::Base ) + k.send(:define_attr_method, :primary_key) { "sys_" + original_primary_key } + assert_equal "sys_id", k.primary_key + end + + def test_set_table_name_with_value + k = Class.new( ActiveRecord::Base ) + k.table_name = "foo" + assert_equal "foo", k.table_name + k.set_table_name "bar" + assert_equal "bar", k.table_name + end + + def test_set_table_name_with_block + k = Class.new( ActiveRecord::Base ) + k.set_table_name { "ks" } + assert_equal "ks", k.table_name + end + + def test_set_primary_key_with_value + k = Class.new( ActiveRecord::Base ) + k.primary_key = "foo" + assert_equal "foo", k.primary_key + k.set_primary_key "bar" + assert_equal "bar", k.primary_key + end + + def test_set_primary_key_with_block + k = Class.new( ActiveRecord::Base ) + k.set_primary_key { "sys_" + original_primary_key } + assert_equal "sys_id", k.primary_key + end + + def test_set_inheritance_column_with_value + k = Class.new( ActiveRecord::Base ) + k.inheritance_column = "foo" + assert_equal "foo", k.inheritance_column + k.set_inheritance_column "bar" + assert_equal "bar", k.inheritance_column + end + + def test_set_inheritance_column_with_block + k = Class.new( ActiveRecord::Base ) + k.set_inheritance_column { original_inheritance_column + "_id" } + assert_equal "type_id", k.inheritance_column + end + + def test_count_with_join + res = Post.count_by_sql "SELECT COUNT(*) FROM posts LEFT JOIN comments ON posts.id=comments.post_id WHERE posts.#{QUOTED_TYPE} = 'Post'" + res2 = nil + assert_nothing_raised do + res2 = Post.count("posts.#{QUOTED_TYPE} = 'Post'", + "LEFT JOIN comments ON posts.id=comments.post_id") + end + assert_equal res, res2 + + res3 = nil + assert_nothing_raised do + res3 = Post.count(:conditions => "posts.#{QUOTED_TYPE} = 'Post'", + :joins => "LEFT JOIN comments ON posts.id=comments.post_id") + end + assert_equal res, res3 + + res4 = Post.count_by_sql "SELECT COUNT(p.id) FROM posts p, comments c WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=c.post_id" + res5 = nil + assert_nothing_raised do + res5 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=c.post_id", + :joins => "p, comments c", + :select => "p.id") + end + + assert_equal res4, res5 + + res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments c WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=c.post_id" + res7 = nil + assert_nothing_raised do + res7 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=c.post_id", + :joins => "p, comments c", + :select => "p.id", + :distinct => true) + end + assert_equal res6, res7 + end + + def test_clear_association_cache_stored + firm = Firm.find(1) + assert_kind_of Firm, firm + + firm.clear_association_cache + assert_equal Firm.find(1).clients.collect{ |x| x.name }.sort, firm.clients.collect{ |x| x.name }.sort + end + + def test_clear_association_cache_new_record + firm = Firm.new + client_stored = Client.find(3) + client_new = Client.new + client_new.name = "The Joneses" + clients = [ client_stored, client_new ] + + firm.clients << clients + + firm.clear_association_cache + + assert_equal firm.clients.collect{ |x| x.name }.sort, clients.collect{ |x| x.name }.sort + end + + def test_interpolate_sql + assert_nothing_raised { Category.new.send(:interpolate_sql, 'foo@bar') } + assert_nothing_raised { Category.new.send(:interpolate_sql, 'foo bar) baz') } + assert_nothing_raised { Category.new.send(:interpolate_sql, 'foo bar} baz') } + end + + def test_scoped_find_conditions + scoped_developers = Developer.with_scope(:find => { :conditions => 'salary > 90000' }) do + Developer.find(:all, :conditions => 'id < 5') + end + assert !scoped_developers.include?(developers(:david)) # David's salary is less than 90,000 + assert_equal 3, scoped_developers.size + end + + def test_scoped_find_limit_offset + scoped_developers = Developer.with_scope(:find => { :limit => 3, :offset => 2 }) do + Developer.find(:all, :order => 'id') + end + assert !scoped_developers.include?(developers(:david)) + assert !scoped_developers.include?(developers(:jamis)) + assert_equal 3, scoped_developers.size + + # Test without scoped find conditions to ensure we get the whole thing + developers = Developer.find(:all, :order => 'id') + assert_equal Developer.count, developers.size + end + + def test_base_class + assert LoosePerson.abstract_class? + assert !LooseDescendant.abstract_class? + assert_equal LoosePerson, LoosePerson.base_class + assert_equal LooseDescendant, LooseDescendant.base_class + assert_equal TightPerson, TightPerson.base_class + assert_equal TightPerson, TightDescendant.base_class + end + + def test_assert_queries + query = lambda { ActiveRecord::Base.connection.execute 'select count(*) from developers' } + assert_queries(2) { 2.times { query.call } } + assert_queries 1, &query + assert_no_queries { assert true } + end + + def test_to_xml + xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true) + bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema + written_on_in_current_timezone = topics(:first).written_on.xmlschema + last_read_in_current_timezone = topics(:first).last_read.xmlschema + assert_equal "", xml.first(7) + assert xml.include?(%(The First Topic)) + assert xml.include?(%(David)) + assert xml.include?(%(1)) + assert xml.include?(%(0)) + assert xml.include?(%(#{written_on_in_current_timezone})) + assert xml.include?(%(Have a nice day)) + assert xml.include?(%(david@loudthinking.com)) + assert xml.include?(%()) + if current_adapter?(:SybaseAdapter) or current_adapter?(:SQLServerAdapter) + assert xml.include?(%(#{last_read_in_current_timezone})) + else + assert xml.include?(%(2004-04-15)) + end + # Oracle and DB2 don't have true boolean or time-only fields + unless current_adapter?(:OracleAdapter) || current_adapter?(:DB2Adapter) + assert xml.include?(%(false)), "Approved should be a boolean" + assert xml.include?(%(#{bonus_time_in_current_timezone})) + end + end + + def test_to_xml_skipping_attributes + xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => :title) + assert_equal "", xml.first(7) + assert !xml.include?(%(The First Topic)) + assert xml.include?(%(David)) + + xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [ :title, :author_name ]) + assert !xml.include?(%(The First Topic)) + assert !xml.include?(%(David)) + end + + def test_to_xml_including_has_many_association + xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies) + assert_equal "", xml.first(7) + assert xml.include?(%()) + assert xml.include?(%(The Second Topic's of the day)) + end + + def test_to_xml_including_belongs_to_association + xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) + assert !xml.include?("") + + xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) + assert xml.include?("") + end + + def test_to_xml_including_multiple_associations + xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ]) + assert_equal "", xml.first(6) + assert xml.include?(%()) + assert xml.include?(%()) + end + + def test_to_xml_including_multiple_associations_with_options + xml = companies(:first_firm).to_xml( + :indent => 0, :skip_instruct => true, + :include => { :clients => { :only => :name } } + ) + + assert_equal "", xml.first(6) + assert xml.include?(%(Summit)) + assert xml.include?(%()) + end + + def test_except_attributes + assert_equal( + %w( author_name type id approved replies_count bonus_time written_on content author_email_address parent_id last_read), + topics(:first).attributes(:except => :title).keys + ) + + assert_equal( + %w( replies_count bonus_time written_on content author_email_address parent_id last_read), + topics(:first).attributes(:except => [ :title, :id, :type, :approved, :author_name ]).keys + ) + end + + def test_include_attributes + assert_equal(%w( title ), topics(:first).attributes(:only => :title).keys) + assert_equal(%w( title author_name type id approved ), topics(:first).attributes(:only => [ :title, :id, :type, :approved, :author_name ]).keys) + end + + def test_type_name_with_module_should_handle_beginning + assert_equal 'ActiveRecord::Person', ActiveRecord::Base.send(:type_name_with_module, 'Person') + assert_equal '::Person', ActiveRecord::Base.send(:type_name_with_module, '::Person') + end + + # FIXME: this test ought to run, but it needs to run sandboxed so that it + # doesn't b0rk the current test environment by undefing everything. + # + #def test_dev_mode_memory_leak + # counts = [] + # 2.times do + # require_dependency 'fixtures/company' + # Firm.find(:first) + # Dependencies.clear + # ActiveRecord::Base.reset_subclasses + # Dependencies.remove_subclasses_for(ActiveRecord::Base) + # + # GC.start + # + # count = 0 + # ObjectSpace.each_object(Proc) { count += 1 } + # counts << count + # end + # assert counts.last <= counts.first, + # "expected last count (#{counts.last}) to be <= first count (#{counts.first})" + #end + + private + def assert_readers(model, exceptions) + expected_readers = Set.new(model.column_names - (model.serialized_attributes.keys + ['id'])) + expected_readers += expected_readers.map { |col| "#{col}?" } + expected_readers -= exceptions + assert_equal expected_readers, model.read_methods + end +end diff --git a/vendor/rails/activerecord/test/binary_test.rb b/vendor/rails/activerecord/test/binary_test.rb new file mode 100644 index 00000000..38a5d509 --- /dev/null +++ b/vendor/rails/activerecord/test/binary_test.rb @@ -0,0 +1,37 @@ +require 'abstract_unit' +require 'fixtures/binary' + +class BinaryTest < Test::Unit::TestCase + BINARY_FIXTURE_PATH = File.dirname(__FILE__) + '/fixtures/flowers.jpg' + + def setup + Binary.connection.execute 'DELETE FROM binaries' + @data = File.read(BINARY_FIXTURE_PATH).freeze + end + + def test_truth + assert true + end + + # Without using prepared statements, it makes no sense to test + # BLOB data with SQL Server, because the length of a statement is + # limited to 8KB. + # + # Without using prepared statements, it makes no sense to test + # BLOB data with DB2 or Firebird, because the length of a statement + # is limited to 32KB. + unless %w(SQLServer Sybase DB2 Oracle Firebird).include? ActiveRecord::Base.connection.adapter_name + def test_load_save + bin = Binary.new + bin.data = @data + + assert @data == bin.data, 'Newly assigned data differs from original' + + bin.save + assert @data == bin.data, 'Data differs from original after save' + + db_bin = Binary.find(bin.id) + assert @data == db_bin.data, 'Reloaded data differs from original' + end + end +end diff --git a/vendor/rails/activerecord/test/calculations_test.rb b/vendor/rails/activerecord/test/calculations_test.rb new file mode 100644 index 00000000..fbf830e9 --- /dev/null +++ b/vendor/rails/activerecord/test/calculations_test.rb @@ -0,0 +1,181 @@ +require 'abstract_unit' +require 'fixtures/company' +require 'fixtures/topic' + +Company.has_many :accounts + +class CalculationsTest < Test::Unit::TestCase + fixtures :companies, :accounts, :topics + + def test_should_sum_field + assert_equal 265, Account.sum(:credit_limit) + end + + def test_should_average_field + value = Account.average(:credit_limit) + assert_equal 53, value + assert_kind_of Float, value + end + + def test_should_get_maximum_of_field + assert_equal 60, Account.maximum(:credit_limit) + end + + def test_should_get_minimum_of_field + assert_equal 50, Account.minimum(:credit_limit) + end + + def test_should_group_by_field + c = Account.sum(:credit_limit, :group => :firm_id) + [1,6,2].each { |firm_id| assert c.keys.include?(firm_id) } + end + + def test_should_group_by_summed_field + c = Account.sum(:credit_limit, :group => :firm_id) + assert_equal 50, c[1] + assert_equal 105, c[6] + assert_equal 60, c[2] + end + + def test_should_order_by_grouped_field + c = Account.sum(:credit_limit, :group => :firm_id, :order => "firm_id") + assert_equal [1, 2, 6], c.keys.compact + end + + def test_should_order_by_calculation + c = Account.sum(:credit_limit, :group => :firm_id, :order => "sum_credit_limit desc, firm_id") + assert_equal [105, 60, 50, 50], c.keys.collect { |k| c[k] } + assert_equal [6, 2, 1], c.keys.compact + end + + def test_should_limit_calculation + c = Account.sum(:credit_limit, :conditions => "firm_id IS NOT NULL", + :group => :firm_id, :order => "firm_id", :limit => 2) + assert_equal [1, 2], c.keys.compact + end + + def test_should_limit_calculation_with_offset + c = Account.sum(:credit_limit, :conditions => "firm_id IS NOT NULL", + :group => :firm_id, :order => "firm_id", :limit => 2, :offset => 1) + assert_equal [2, 6], c.keys.compact + end + + def test_should_group_by_summed_field_having_condition + c = Account.sum(:credit_limit, :group => :firm_id, + :having => 'sum(credit_limit) > 50') + assert_nil c[1] + assert_equal 105, c[6] + assert_equal 60, c[2] + end + + def test_should_group_by_summed_association + c = Account.sum(:credit_limit, :group => :firm) + assert_equal 50, c[companies(:first_firm)] + assert_equal 105, c[companies(:rails_core)] + assert_equal 60, c[companies(:first_client)] + end + + def test_should_sum_field_with_conditions + assert_equal 105, Account.sum(:credit_limit, :conditions => 'firm_id = 6') + end + + def test_should_group_by_summed_field_with_conditions + c = Account.sum(:credit_limit, :conditions => 'firm_id > 1', + :group => :firm_id) + assert_nil c[1] + assert_equal 105, c[6] + assert_equal 60, c[2] + end + + def test_should_group_by_summed_field_with_conditions_and_having + c = Account.sum(:credit_limit, :conditions => 'firm_id > 1', + :group => :firm_id, + :having => 'sum(credit_limit) > 60') + assert_nil c[1] + assert_equal 105, c[6] + assert_nil c[2] + end + + def test_should_group_by_fields_with_table_alias + c = Account.sum(:credit_limit, :group => 'accounts.firm_id') + assert_equal 50, c[1] + assert_equal 105, c[6] + assert_equal 60, c[2] + end + + def test_should_calculate_with_invalid_field + assert_equal 5, Account.calculate(:count, '*') + assert_equal 5, Account.calculate(:count, :all) + end + + def test_should_calculate_grouped_with_invalid_field + c = Account.count(:all, :group => 'accounts.firm_id') + assert_equal 1, c[1] + assert_equal 2, c[6] + assert_equal 1, c[2] + end + + def test_should_calculate_grouped_association_with_invalid_field + c = Account.count(:all, :group => :firm) + assert_equal 1, c[companies(:first_firm)] + assert_equal 2, c[companies(:rails_core)] + assert_equal 1, c[companies(:first_client)] + end + + def test_should_calculate_grouped_by_function + c = Company.count(:all, :group => 'UPPER(type)') + assert_equal 2, c[nil] + assert_equal 1, c['DEPENDENTFIRM'] + assert_equal 3, c['CLIENT'] + assert_equal 2, c['FIRM'] + end + + def test_should_calculate_grouped_by_function_with_table_alias + c = Company.count(:all, :group => 'UPPER(companies.type)') + assert_equal 2, c[nil] + assert_equal 1, c['DEPENDENTFIRM'] + assert_equal 3, c['CLIENT'] + assert_equal 2, c['FIRM'] + end + + def test_should_sum_scoped_field + assert_equal 15, companies(:rails_core).companies.sum(:id) + end + + def test_should_sum_scoped_field_with_conditions + assert_equal 8, companies(:rails_core).companies.sum(:id, :conditions => 'id > 7') + end + + def test_should_group_by_scoped_field + c = companies(:rails_core).companies.sum(:id, :group => :name) + assert_equal 7, c['Leetsoft'] + assert_equal 8, c['Jadedpixel'] + end + + def test_should_group_by_summed_field_with_conditions_and_having + c = companies(:rails_core).companies.sum(:id, :group => :name, + :having => 'sum(id) > 7') + assert_nil c['Leetsoft'] + assert_equal 8, c['Jadedpixel'] + end + + def test_should_reject_invalid_options + assert_nothing_raised do + [:count, :sum].each do |func| + # empty options are valid + Company.send(:validate_calculation_options, func) + # these options are valid for all calculations + [:select, :conditions, :joins, :order, :group, :having, :distinct].each do |opt| + Company.send(:validate_calculation_options, func, opt => true) + end + end + + # :include is only valid on :count + Company.send(:validate_calculation_options, :count, :include => true) + end + + assert_raises(ArgumentError) { Company.send(:validate_calculation_options, :sum, :include => :posts) } + assert_raises(ArgumentError) { Company.send(:validate_calculation_options, :sum, :foo => :bar) } + assert_raises(ArgumentError) { Company.send(:validate_calculation_options, :count, :foo => :bar) } + end +end diff --git a/vendor/rails/activerecord/test/callbacks_test.rb b/vendor/rails/activerecord/test/callbacks_test.rb new file mode 100644 index 00000000..3a4dac90 --- /dev/null +++ b/vendor/rails/activerecord/test/callbacks_test.rb @@ -0,0 +1,364 @@ +require 'abstract_unit' + +class CallbackDeveloper < ActiveRecord::Base + set_table_name 'developers' + + class << self + def callback_string(callback_method) + "history << [#{callback_method.to_sym.inspect}, :string]" + end + + def callback_proc(callback_method) + Proc.new { |model| model.history << [callback_method, :proc] } + end + + def define_callback_method(callback_method) + define_method("#{callback_method}_method") do |model| + model.history << [callback_method, :method] + end + end + + def callback_object(callback_method) + klass = Class.new + klass.send(:define_method, callback_method) do |model| + model.history << [callback_method, :object] + end + klass.new + end + end + + ActiveRecord::Callbacks::CALLBACKS.each do |callback_method| + callback_method_sym = callback_method.to_sym + define_callback_method(callback_method_sym) + send(callback_method, callback_method_sym) + send(callback_method, callback_string(callback_method_sym)) + send(callback_method, callback_proc(callback_method_sym)) + send(callback_method, callback_object(callback_method_sym)) + send(callback_method) { |model| model.history << [callback_method_sym, :block] } + end + + def history + @history ||= [] + end + + # after_initialize and after_find are invoked only if instance methods have been defined. + def after_initialize + end + + def after_find + end +end + +class RecursiveCallbackDeveloper < ActiveRecord::Base + set_table_name 'developers' + + before_save :on_before_save + after_save :on_after_save + + attr_reader :on_before_save_called, :on_after_save_called + + def on_before_save + @on_before_save_called ||= 0 + @on_before_save_called += 1 + save unless @on_before_save_called > 1 + end + + def on_after_save + @on_after_save_called ||= 0 + @on_after_save_called += 1 + save unless @on_after_save_called > 1 + end +end + +class ImmutableDeveloper < ActiveRecord::Base + set_table_name 'developers' + + validates_inclusion_of :salary, :in => 50000..200000 + + before_save :cancel + before_destroy :cancel + + def cancelled? + @cancelled == true + end + + private + def cancel + @cancelled = true + false + end +end + +class ImmutableMethodDeveloper < ActiveRecord::Base + set_table_name 'developers' + + validates_inclusion_of :salary, :in => 50000..200000 + + def cancelled? + @cancelled == true + end + + def before_save + @cancelled = true + false + end + + def before_destroy + @cancelled = true + false + end +end + +class CallbacksTest < Test::Unit::TestCase + fixtures :developers + + def test_initialize + david = CallbackDeveloper.new + assert_equal [ + [ :after_initialize, :string ], + [ :after_initialize, :proc ], + [ :after_initialize, :object ], + [ :after_initialize, :block ], + ], david.history + end + + def test_find + david = CallbackDeveloper.find(1) + assert_equal [ + [ :after_find, :string ], + [ :after_find, :proc ], + [ :after_find, :object ], + [ :after_find, :block ], + [ :after_initialize, :string ], + [ :after_initialize, :proc ], + [ :after_initialize, :object ], + [ :after_initialize, :block ], + ], david.history + end + + def test_new_valid? + david = CallbackDeveloper.new + david.valid? + assert_equal [ + [ :after_initialize, :string ], + [ :after_initialize, :proc ], + [ :after_initialize, :object ], + [ :after_initialize, :block ], + [ :before_validation, :string ], + [ :before_validation, :proc ], + [ :before_validation, :object ], + [ :before_validation, :block ], + [ :before_validation_on_create, :string ], + [ :before_validation_on_create, :proc ], + [ :before_validation_on_create, :object ], + [ :before_validation_on_create, :block ], + [ :after_validation, :string ], + [ :after_validation, :proc ], + [ :after_validation, :object ], + [ :after_validation, :block ], + [ :after_validation_on_create, :string ], + [ :after_validation_on_create, :proc ], + [ :after_validation_on_create, :object ], + [ :after_validation_on_create, :block ] + ], david.history + end + + def test_existing_valid? + david = CallbackDeveloper.find(1) + david.valid? + assert_equal [ + [ :after_find, :string ], + [ :after_find, :proc ], + [ :after_find, :object ], + [ :after_find, :block ], + [ :after_initialize, :string ], + [ :after_initialize, :proc ], + [ :after_initialize, :object ], + [ :after_initialize, :block ], + [ :before_validation, :string ], + [ :before_validation, :proc ], + [ :before_validation, :object ], + [ :before_validation, :block ], + [ :before_validation_on_update, :string ], + [ :before_validation_on_update, :proc ], + [ :before_validation_on_update, :object ], + [ :before_validation_on_update, :block ], + [ :after_validation, :string ], + [ :after_validation, :proc ], + [ :after_validation, :object ], + [ :after_validation, :block ], + [ :after_validation_on_update, :string ], + [ :after_validation_on_update, :proc ], + [ :after_validation_on_update, :object ], + [ :after_validation_on_update, :block ] + ], david.history + end + + def test_create + david = CallbackDeveloper.create('name' => 'David', 'salary' => 1000000) + assert_equal [ + [ :after_initialize, :string ], + [ :after_initialize, :proc ], + [ :after_initialize, :object ], + [ :after_initialize, :block ], + [ :before_validation, :string ], + [ :before_validation, :proc ], + [ :before_validation, :object ], + [ :before_validation, :block ], + [ :before_validation_on_create, :string ], + [ :before_validation_on_create, :proc ], + [ :before_validation_on_create, :object ], + [ :before_validation_on_create, :block ], + [ :after_validation, :string ], + [ :after_validation, :proc ], + [ :after_validation, :object ], + [ :after_validation, :block ], + [ :after_validation_on_create, :string ], + [ :after_validation_on_create, :proc ], + [ :after_validation_on_create, :object ], + [ :after_validation_on_create, :block ], + [ :before_save, :string ], + [ :before_save, :proc ], + [ :before_save, :object ], + [ :before_save, :block ], + [ :before_create, :string ], + [ :before_create, :proc ], + [ :before_create, :object ], + [ :before_create, :block ], + [ :after_create, :string ], + [ :after_create, :proc ], + [ :after_create, :object ], + [ :after_create, :block ], + [ :after_save, :string ], + [ :after_save, :proc ], + [ :after_save, :object ], + [ :after_save, :block ] + ], david.history + end + + def test_save + david = CallbackDeveloper.find(1) + david.save + assert_equal [ + [ :after_find, :string ], + [ :after_find, :proc ], + [ :after_find, :object ], + [ :after_find, :block ], + [ :after_initialize, :string ], + [ :after_initialize, :proc ], + [ :after_initialize, :object ], + [ :after_initialize, :block ], + [ :before_validation, :string ], + [ :before_validation, :proc ], + [ :before_validation, :object ], + [ :before_validation, :block ], + [ :before_validation_on_update, :string ], + [ :before_validation_on_update, :proc ], + [ :before_validation_on_update, :object ], + [ :before_validation_on_update, :block ], + [ :after_validation, :string ], + [ :after_validation, :proc ], + [ :after_validation, :object ], + [ :after_validation, :block ], + [ :after_validation_on_update, :string ], + [ :after_validation_on_update, :proc ], + [ :after_validation_on_update, :object ], + [ :after_validation_on_update, :block ], + [ :before_save, :string ], + [ :before_save, :proc ], + [ :before_save, :object ], + [ :before_save, :block ], + [ :before_update, :string ], + [ :before_update, :proc ], + [ :before_update, :object ], + [ :before_update, :block ], + [ :after_update, :string ], + [ :after_update, :proc ], + [ :after_update, :object ], + [ :after_update, :block ], + [ :after_save, :string ], + [ :after_save, :proc ], + [ :after_save, :object ], + [ :after_save, :block ] + ], david.history + end + + def test_destroy + david = CallbackDeveloper.find(1) + david.destroy + assert_equal [ + [ :after_find, :string ], + [ :after_find, :proc ], + [ :after_find, :object ], + [ :after_find, :block ], + [ :after_initialize, :string ], + [ :after_initialize, :proc ], + [ :after_initialize, :object ], + [ :after_initialize, :block ], + [ :before_destroy, :string ], + [ :before_destroy, :proc ], + [ :before_destroy, :object ], + [ :before_destroy, :block ], + [ :after_destroy, :string ], + [ :after_destroy, :proc ], + [ :after_destroy, :object ], + [ :after_destroy, :block ] + ], david.history + end + + def test_delete + david = CallbackDeveloper.find(1) + CallbackDeveloper.delete(david.id) + assert_equal [ + [ :after_find, :string ], + [ :after_find, :proc ], + [ :after_find, :object ], + [ :after_find, :block ], + [ :after_initialize, :string ], + [ :after_initialize, :proc ], + [ :after_initialize, :object ], + [ :after_initialize, :block ], + ], david.history + end + + def test_before_save_returning_false + david = ImmutableDeveloper.find(1) + assert david.valid? + assert !david.save + assert_raises(ActiveRecord::RecordNotSaved) { david.save! } + + david = ImmutableDeveloper.find(1) + david.salary = 10_000_000 + assert !david.valid? + assert !david.save + assert_raises(ActiveRecord::RecordInvalid) { david.save! } + end + + def test_before_destroy_returning_false + david = ImmutableDeveloper.find(1) + assert !david.destroy + assert_not_nil ImmutableDeveloper.find_by_id(1) + end + + def test_zzz_callback_returning_false # must be run last since we modify CallbackDeveloper + david = CallbackDeveloper.find(1) + CallbackDeveloper.before_validation proc { |model| model.history << [:before_validation, :returning_false]; return false } + CallbackDeveloper.before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] } + david.save + assert_equal [ + [ :after_find, :string ], + [ :after_find, :proc ], + [ :after_find, :object ], + [ :after_find, :block ], + [ :after_initialize, :string ], + [ :after_initialize, :proc ], + [ :after_initialize, :object ], + [ :after_initialize, :block ], + [ :before_validation, :string ], + [ :before_validation, :proc ], + [ :before_validation, :object ], + [ :before_validation, :block ], + [ :before_validation, :returning_false ] + ], david.history + end +end diff --git a/vendor/rails/activerecord/test/class_inheritable_attributes_test.rb b/vendor/rails/activerecord/test/class_inheritable_attributes_test.rb new file mode 100644 index 00000000..a3f94e3a --- /dev/null +++ b/vendor/rails/activerecord/test/class_inheritable_attributes_test.rb @@ -0,0 +1,32 @@ +require 'test/unit' +require 'abstract_unit' +require 'active_support/core_ext/class/inheritable_attributes' + +class A + include ClassInheritableAttributes +end + +class B < A + write_inheritable_array "first", [ :one, :two ] +end + +class C < A + write_inheritable_array "first", [ :three ] +end + +class D < B + write_inheritable_array "first", [ :four ] +end + + +class ClassInheritableAttributesTest < Test::Unit::TestCase + def test_first_level + assert_equal [ :one, :two ], B.read_inheritable_attribute("first") + assert_equal [ :three ], C.read_inheritable_attribute("first") + end + + def test_second_level + assert_equal [ :one, :two, :four ], D.read_inheritable_attribute("first") + assert_equal [ :one, :two ], B.read_inheritable_attribute("first") + end +end diff --git a/vendor/rails/activerecord/test/column_alias_test.rb b/vendor/rails/activerecord/test/column_alias_test.rb new file mode 100644 index 00000000..19526cf9 --- /dev/null +++ b/vendor/rails/activerecord/test/column_alias_test.rb @@ -0,0 +1,17 @@ +require 'abstract_unit' +require 'fixtures/topic' + +class TestColumnAlias < Test::Unit::TestCase + fixtures :topics + + QUERY = if 'Oracle' == ActiveRecord::Base.connection.adapter_name + 'SELECT id AS pk FROM topics WHERE ROWNUM < 2' + else + 'SELECT id AS pk FROM topics' + end + + def test_column_alias + records = Topic.connection.select_all(QUERY) + assert_equal 'pk', records[0].keys[0] + end +end diff --git a/vendor/rails/activerecord/test/connections/native_db2/connection.rb b/vendor/rails/activerecord/test/connections/native_db2/connection.rb new file mode 100644 index 00000000..aa736ccc --- /dev/null +++ b/vendor/rails/activerecord/test/connections/native_db2/connection.rb @@ -0,0 +1,24 @@ +print "Using native DB2\n" +require_dependency 'fixtures/course' +require 'logger' + +ActiveRecord::Base.logger = Logger.new("debug.log") + +db1 = 'arunit' +db2 = 'arunit2' + +ActiveRecord::Base.establish_connection( + :adapter => "db2", + :host => "localhost", + :username => "arunit", + :password => "arunit", + :database => db1 +) + +Course.establish_connection( + :adapter => "db2", + :host => "localhost", + :username => "arunit2", + :password => "arunit2", + :database => db2 +) diff --git a/vendor/rails/activerecord/test/connections/native_firebird/connection.rb b/vendor/rails/activerecord/test/connections/native_firebird/connection.rb new file mode 100644 index 00000000..c861d952 --- /dev/null +++ b/vendor/rails/activerecord/test/connections/native_firebird/connection.rb @@ -0,0 +1,24 @@ +print "Using native Firebird\n" +require_dependency 'fixtures/course' +require 'logger' + +ActiveRecord::Base.logger = Logger.new("debug.log") + +db1 = 'activerecord_unittest' +db2 = 'activerecord_unittest2' + +ActiveRecord::Base.establish_connection( + :adapter => "firebird", + :host => "localhost", + :username => "rails", + :password => "rails", + :database => db1 +) + +Course.establish_connection( + :adapter => "firebird", + :host => "localhost", + :username => "rails", + :password => "rails", + :database => db2 +) diff --git a/vendor/rails/activerecord/test/connections/native_mysql/connection.rb b/vendor/rails/activerecord/test/connections/native_mysql/connection.rb new file mode 100644 index 00000000..b665a6b4 --- /dev/null +++ b/vendor/rails/activerecord/test/connections/native_mysql/connection.rb @@ -0,0 +1,21 @@ +print "Using native MySQL\n" +require_dependency 'fixtures/course' +require 'logger' + +ActiveRecord::Base.logger = Logger.new("debug.log") + +db1 = 'activerecord_unittest' +db2 = 'activerecord_unittest2' + +ActiveRecord::Base.establish_connection( + :adapter => "mysql", + :username => "rails", + :encoding => "utf8", + :database => db1 +) + +Course.establish_connection( + :adapter => "mysql", + :username => "rails", + :database => db2 +) diff --git a/vendor/rails/activerecord/test/connections/native_openbase/connection.rb b/vendor/rails/activerecord/test/connections/native_openbase/connection.rb new file mode 100644 index 00000000..01a2ffe6 --- /dev/null +++ b/vendor/rails/activerecord/test/connections/native_openbase/connection.rb @@ -0,0 +1,22 @@ +print "Using native OpenBase\n" +require_dependency 'fixtures/course' +require 'logger' + +ActiveRecord::Base.logger = Logger.new("debug.log") + +db1 = 'activerecord_unittest' +db2 = 'activerecord_unittest2' + +ActiveRecord::Base.establish_connection( + :adapter => "openbase", + :username => "admin", + :password => "", + :database => db1 +) + +Course.establish_connection( + :adapter => "openbase", + :username => "admin", + :password => "", + :database => db2 +) diff --git a/vendor/rails/activerecord/test/connections/native_oracle/connection.rb b/vendor/rails/activerecord/test/connections/native_oracle/connection.rb new file mode 100644 index 00000000..b0d9696a --- /dev/null +++ b/vendor/rails/activerecord/test/connections/native_oracle/connection.rb @@ -0,0 +1,23 @@ +print "Using Oracle\n" +require_dependency 'fixtures/course' +require 'logger' + +ActiveRecord::Base.logger = Logger.new STDOUT +ActiveRecord::Base.logger.level = Logger::WARN + +# Set these to your database connection strings +db = ENV['ARUNIT_DB'] || 'activerecord_unittest' + +ActiveRecord::Base.establish_connection( + :adapter => 'oracle', + :username => 'arunit', + :password => 'arunit', + :database => db +) + +Course.establish_connection( + :adapter => 'oracle', + :username => 'arunit2', + :password => 'arunit2', + :database => db +) diff --git a/vendor/rails/activerecord/test/connections/native_postgresql/connection.rb b/vendor/rails/activerecord/test/connections/native_postgresql/connection.rb new file mode 100644 index 00000000..1bdff730 --- /dev/null +++ b/vendor/rails/activerecord/test/connections/native_postgresql/connection.rb @@ -0,0 +1,24 @@ +print "Using native PostgreSQL\n" +require_dependency 'fixtures/course' +require 'logger' + +ActiveRecord::Base.logger = Logger.new("debug.log") + +db1 = 'activerecord_unittest' +db2 = 'activerecord_unittest2' + +ActiveRecord::Base.establish_connection( + :adapter => "postgresql", + :username => "postgres", + :password => "postgres", + :database => db1, + :min_messages => "warning" +) + +Course.establish_connection( + :adapter => "postgresql", + :username => "postgres", + :password => "postgres", + :database => db2, + :min_messages => "warning" +) diff --git a/vendor/rails/activerecord/test/connections/native_sqlite/connection.rb b/vendor/rails/activerecord/test/connections/native_sqlite/connection.rb new file mode 100644 index 00000000..b714e178 --- /dev/null +++ b/vendor/rails/activerecord/test/connections/native_sqlite/connection.rb @@ -0,0 +1,37 @@ +print "Using native SQlite\n" +require_dependency 'fixtures/course' +require 'logger' +ActiveRecord::Base.logger = Logger.new("debug.log") + +class SqliteError < StandardError +end + +BASE_DIR = File.expand_path(File.dirname(__FILE__) + '/../../fixtures') +sqlite_test_db = "#{BASE_DIR}/fixture_database.sqlite" +sqlite_test_db2 = "#{BASE_DIR}/fixture_database_2.sqlite" + +def make_connection(clazz, db_file, db_definitions_file) + unless File.exist?(db_file) + puts "SQLite database not found at #{db_file}. Rebuilding it." + sqlite_command = %Q{sqlite #{db_file} "create table a (a integer); drop table a;"} + puts "Executing '#{sqlite_command}'" + raise SqliteError.new("Seems that there is no sqlite executable available") unless system(sqlite_command) + clazz.establish_connection( + :adapter => "sqlite", + :database => db_file) + script = File.read("#{BASE_DIR}/db_definitions/#{db_definitions_file}") + # SQLite-Ruby has problems with semi-colon separated commands, so split and execute one at a time + script.split(';').each do + |command| + clazz.connection.execute(command) unless command.strip.empty? + end + else + clazz.establish_connection( + :adapter => "sqlite", + :database => db_file) + end +end + +make_connection(ActiveRecord::Base, sqlite_test_db, 'sqlite.sql') +make_connection(Course, sqlite_test_db2, 'sqlite2.sql') +load(File.join(BASE_DIR, 'db_definitions', 'schema.rb')) diff --git a/vendor/rails/activerecord/test/connections/native_sqlite3/connection.rb b/vendor/rails/activerecord/test/connections/native_sqlite3/connection.rb new file mode 100644 index 00000000..182ec6ba --- /dev/null +++ b/vendor/rails/activerecord/test/connections/native_sqlite3/connection.rb @@ -0,0 +1,37 @@ +print "Using native SQLite3\n" +require_dependency 'fixtures/course' +require 'logger' +ActiveRecord::Base.logger = Logger.new("debug.log") + +class SqliteError < StandardError +end + +BASE_DIR = File.expand_path(File.dirname(__FILE__) + '/../../fixtures') +sqlite_test_db = "#{BASE_DIR}/fixture_database.sqlite3" +sqlite_test_db2 = "#{BASE_DIR}/fixture_database_2.sqlite3" + +def make_connection(clazz, db_file, db_definitions_file) + unless File.exist?(db_file) + puts "SQLite3 database not found at #{db_file}. Rebuilding it." + sqlite_command = %Q{sqlite3 #{db_file} "create table a (a integer); drop table a;"} + puts "Executing '#{sqlite_command}'" + raise SqliteError.new("Seems that there is no sqlite3 executable available") unless system(sqlite_command) + clazz.establish_connection( + :adapter => "sqlite3", + :database => db_file) + script = File.read("#{BASE_DIR}/db_definitions/#{db_definitions_file}") + # SQLite-Ruby has problems with semi-colon separated commands, so split and execute one at a time + script.split(';').each do + |command| + clazz.connection.execute(command) unless command.strip.empty? + end + else + clazz.establish_connection( + :adapter => "sqlite3", + :database => db_file) + end +end + +make_connection(ActiveRecord::Base, sqlite_test_db, 'sqlite.sql') +make_connection(Course, sqlite_test_db2, 'sqlite2.sql') +load(File.join(BASE_DIR, 'db_definitions', 'schema.rb')) diff --git a/vendor/rails/activerecord/test/connections/native_sqlite3/in_memory_connection.rb b/vendor/rails/activerecord/test/connections/native_sqlite3/in_memory_connection.rb new file mode 100644 index 00000000..32bf6a27 --- /dev/null +++ b/vendor/rails/activerecord/test/connections/native_sqlite3/in_memory_connection.rb @@ -0,0 +1,18 @@ +print "Using native SQLite3\n" +require_dependency 'fixtures/course' +require 'logger' +ActiveRecord::Base.logger = Logger.new("debug.log") + +class SqliteError < StandardError +end + +def make_connection(clazz, db_definitions_file) + clazz.establish_connection(:adapter => 'sqlite3', :database => ':memory:') + File.read("#{File.dirname(__FILE__)}/../../fixtures/db_definitions/#{db_definitions_file}").split(';').each do |command| + clazz.connection.execute(command) unless command.strip.empty? + end +end + +make_connection(ActiveRecord::Base, 'sqlite.sql') +make_connection(Course, 'sqlite2.sql') + load("#{File.dirname(__FILE__)}/../../fixtures/db_definitions/schema.rb")) diff --git a/vendor/rails/activerecord/test/connections/native_sqlserver/connection.rb b/vendor/rails/activerecord/test/connections/native_sqlserver/connection.rb new file mode 100644 index 00000000..24658d71 --- /dev/null +++ b/vendor/rails/activerecord/test/connections/native_sqlserver/connection.rb @@ -0,0 +1,24 @@ +print "Using native SQLServer\n" +require_dependency 'fixtures/course' +require 'logger' + +ActiveRecord::Base.logger = Logger.new("debug.log") + +db1 = 'activerecord_unittest' +db2 = 'activerecord_unittest2' + +ActiveRecord::Base.establish_connection( + :adapter => "sqlserver", + :host => "localhost", + :username => "sa", + :password => "", + :database => db1 +) + +Course.establish_connection( + :adapter => "sqlserver", + :host => "localhost", + :username => "sa", + :password => "", + :database => db2 +) diff --git a/vendor/rails/activerecord/test/connections/native_sqlserver_odbc/connection.rb b/vendor/rails/activerecord/test/connections/native_sqlserver_odbc/connection.rb new file mode 100644 index 00000000..918be3ed --- /dev/null +++ b/vendor/rails/activerecord/test/connections/native_sqlserver_odbc/connection.rb @@ -0,0 +1,26 @@ +print "Using native SQLServer via ODBC\n" +require_dependency 'fixtures/course' +require 'logger' + +ActiveRecord::Base.logger = Logger.new("debug.log") + +dsn1 = 'activerecord_unittest' +dsn2 = 'activerecord_unittest2' + +ActiveRecord::Base.establish_connection( + :adapter => "sqlserver", + :mode => "ODBC", + :host => "localhost", + :username => "sa", + :password => "", + :dsn => dsn1 +) + +Course.establish_connection( + :adapter => "sqlserver", + :mode => "ODBC", + :host => "localhost", + :username => "sa", + :password => "", + :dsn => dsn2 +) diff --git a/vendor/rails/activerecord/test/connections/native_sybase/connection.rb b/vendor/rails/activerecord/test/connections/native_sybase/connection.rb new file mode 100644 index 00000000..a3ecf853 --- /dev/null +++ b/vendor/rails/activerecord/test/connections/native_sybase/connection.rb @@ -0,0 +1,24 @@ +print "Using native Sybase Open Client\n" +require_dependency 'fixtures/course' +require 'logger' + +ActiveRecord::Base.logger = Logger.new("debug.log") + +db1 = 'activerecord_unittest' +db2 = 'activerecord_unittest2' + +ActiveRecord::Base.establish_connection( + :adapter => "sybase", + :host => "database_ASE", + :username => "sa", + :password => "", + :database => db1 +) + +Course.establish_connection( + :adapter => "sybase", + :host => "database_ASE", + :username => "sa", + :password => "", + :database => db2 +) diff --git a/vendor/rails/activerecord/test/copy_table_sqlite.rb b/vendor/rails/activerecord/test/copy_table_sqlite.rb new file mode 100644 index 00000000..f3f5f1ce --- /dev/null +++ b/vendor/rails/activerecord/test/copy_table_sqlite.rb @@ -0,0 +1,64 @@ +require 'abstract_unit' + +class CopyTableTest < Test::Unit::TestCase + fixtures :companies, :comments + + def setup + @connection = ActiveRecord::Base.connection + class << @connection + public :copy_table, :table_structure, :indexes + end + end + + def test_copy_table(from = 'companies', to = 'companies2', options = {}) + assert_nothing_raised {copy_table(from, to, options)} + assert_equal row_count(from), row_count(to) + + if block_given? + yield from, to, options + else + assert_equal column_names(from), column_names(to) + end + + @connection.drop_table(to) rescue nil + end + + def test_copy_table_renaming_column + test_copy_table('companies', 'companies2', + :rename => {'client_of' => 'fan_of'}) do |from, to, options| + assert_equal column_values(from, 'client_of').compact.sort, + column_values(to, 'fan_of').compact.sort + end + end + + def test_copy_table_with_index + test_copy_table('comments', 'comments_with_index') do + @connection.add_index('comments_with_index', ['post_id', 'type']) + test_copy_table('comments_with_index', 'comments_with_index2') do + assert_equal table_indexes_without_name('comments_with_index'), + table_indexes_without_name('comments_with_index2') + end + end + end + +protected + def copy_table(from, to, options = {}) + @connection.copy_table(from, to, {:temporary => true}.merge(options)) + end + + def column_names(table) + @connection.table_structure(table).map {|column| column['name']} + end + + def column_values(table, column) + @connection.select_all("SELECT #{column} FROM #{table}").map {|row| row[column]} + end + + def table_indexes_without_name(table) + @connection.indexes('comments_with_index').delete(:name) + end + + def row_count(table) + @connection.select_one("SELECT COUNT(*) AS count FROM #{table}")['count'] + end +end diff --git a/vendor/rails/activerecord/test/default_test_firebird.rb b/vendor/rails/activerecord/test/default_test_firebird.rb new file mode 100644 index 00000000..4f3d14ce --- /dev/null +++ b/vendor/rails/activerecord/test/default_test_firebird.rb @@ -0,0 +1,16 @@ +require 'abstract_unit' +require 'fixtures/default' + +class DefaultTest < Test::Unit::TestCase + def test_default_timestamp + default = Default.new + assert_instance_of(Time, default.default_timestamp) + assert_equal(:datetime, default.column_for_attribute(:default_timestamp).type) + + # Variance should be small; increase if required -- e.g., if test db is on + # remote host and clocks aren't synchronized. + t1 = Time.new + accepted_variance = 1.0 + assert_in_delta(t1.to_f, default.default_timestamp.to_f, accepted_variance) + end +end diff --git a/vendor/rails/activerecord/test/defaults_test.rb b/vendor/rails/activerecord/test/defaults_test.rb new file mode 100644 index 00000000..f51f77cd --- /dev/null +++ b/vendor/rails/activerecord/test/defaults_test.rb @@ -0,0 +1,18 @@ +require 'abstract_unit' +require 'fixtures/default' + +class DefaultsTest < Test::Unit::TestCase + if %w(PostgreSQL).include? ActiveRecord::Base.connection.adapter_name + def test_default_integers + default = Default.new + assert_instance_of(Fixnum, default.positive_integer) + assert_equal(default.positive_integer, 1) + assert_instance_of(Fixnum, default.negative_integer) + assert_equal(default.negative_integer, -1) + end + else + def test_dummy + assert true + end + end +end diff --git a/vendor/rails/activerecord/test/deprecated_associations_test.rb b/vendor/rails/activerecord/test/deprecated_associations_test.rb new file mode 100755 index 00000000..d3abe145 --- /dev/null +++ b/vendor/rails/activerecord/test/deprecated_associations_test.rb @@ -0,0 +1,352 @@ +require 'abstract_unit' +require 'fixtures/developer' +require 'fixtures/project' +require 'fixtures/company' +require 'fixtures/topic' +require 'fixtures/reply' + +# Can't declare new classes in test case methods, so tests before that +bad_collection_keys = false +begin + class Car < ActiveRecord::Base; has_many :wheels, :name => "wheels"; end +rescue ArgumentError + bad_collection_keys = true +end +raise "ActiveRecord should have barked on bad collection keys" unless bad_collection_keys + + +class DeprecatedAssociationsTest < Test::Unit::TestCase + fixtures :accounts, :companies, :developers, :projects, :topics, + :developers_projects + + def test_has_many_find + assert_equal 2, Firm.find_first.clients.length + end + + def test_has_many_orders + assert_equal "Summit", Firm.find_first.clients.first.name + end + + def test_has_many_class_name + assert_equal "Microsoft", Firm.find_first.clients_sorted_desc.first.name + end + + def test_has_many_foreign_key + assert_equal "Microsoft", Firm.find_first.clients_of_firm.first.name + end + + def test_has_many_conditions + assert_equal "Microsoft", Firm.find_first.clients_like_ms.first.name + end + + def test_has_many_sql + firm = Firm.find_first + assert_equal "Microsoft", firm.clients_using_sql.first.name + assert_equal 1, firm.clients_using_sql_count + assert_equal 1, Firm.find_first.clients_using_sql_count + end + + def test_has_many_counter_sql + assert_equal 1, Firm.find_first.clients_using_counter_sql_count + end + + def test_has_many_queries + assert Firm.find_first.has_clients? + firm = Firm.find_first + assert_equal 2, firm.clients_count # tests using class count + firm.clients + assert firm.has_clients? + assert_equal 2, firm.clients_count # tests using collection length + end + + def test_has_many_dependence + assert_equal 3, Client.find_all.length + Firm.find_first.destroy + assert_equal 1, Client.find_all.length + end + + uses_transaction :test_has_many_dependence_with_transaction_support_on_failure + def test_has_many_dependence_with_transaction_support_on_failure + assert_equal 3, Client.find_all.length + + firm = Firm.find_first + clients = firm.clients + clients.last.instance_eval { def before_destroy() raise "Trigger rollback" end } + + firm.destroy rescue "do nothing" + + assert_equal 3, Client.find_all.length + end + + def test_has_one_dependence + num_accounts = Account.count + firm = Firm.find(1) + assert firm.has_account? + firm.destroy + assert_equal num_accounts - 1, Account.count + end + + def test_has_one_dependence_with_missing_association + Account.destroy_all + firm = Firm.find(1) + assert !firm.has_account? + firm.destroy + end + + def test_belongs_to + assert_equal companies(:first_firm).name, Client.find(3).firm.name + assert Client.find(3).has_firm?, "Microsoft should have a firm" + # assert !Company.find(1).has_firm?, "37signals shouldn't have a firm" + end + + def test_belongs_to_with_different_class_name + assert_equal Company.find(1).name, Company.find(3).firm_with_other_name.name + assert Company.find(3).has_firm_with_other_name?, "Microsoft should have a firm" + end + + def test_belongs_to_with_condition + assert_equal Company.find(1).name, Company.find(3).firm_with_condition.name + assert Company.find(3).has_firm_with_condition?, "Microsoft should have a firm" + end + + def test_belongs_to_equality + assert Company.find(3).firm?(Company.find(1)), "Microsoft should have 37signals as firm" + assert_raises(RuntimeError) { !Company.find(3).firm?(Company.find(3)) } # "Summit shouldn't have itself as firm" + end + + def test_has_one + assert companies(:first_firm).account?(Account.find(1)) + assert_equal Account.find(1).credit_limit, companies(:first_firm).account.credit_limit + assert companies(:first_firm).has_account?, "37signals should have an account" + assert Account.find(1).firm?(companies(:first_firm)), "37signals account should be able to backtrack" + assert Account.find(1).has_firm?, "37signals account should be able to backtrack" + + assert !Account.find(2).has_firm?, "Unknown isn't linked" + assert !Account.find(2).firm?(companies(:first_firm)), "Unknown isn't linked" + end + + def test_has_many_dependence_on_account + num_accounts = Account.count + companies(:first_firm).destroy + assert_equal num_accounts - 1, Account.count + end + + def test_find_in + assert_equal Client.find(2).name, companies(:first_firm).find_in_clients(2).name + assert_raises(ActiveRecord::RecordNotFound) { companies(:first_firm).find_in_clients(6) } + end + + def test_force_reload + firm = Firm.new("name" => "A New Firm, Inc") + firm.save + firm.clients.each {|c|} # forcing to load all clients + assert firm.clients.empty?, "New firm shouldn't have client objects" + assert !firm.has_clients?, "New firm shouldn't have clients" + assert_equal 0, firm.clients_count, "New firm should have 0 clients" + + client = Client.new("name" => "TheClient.com", "firm_id" => firm.id) + client.save + + assert firm.clients.empty?, "New firm should have cached no client objects" + assert !firm.has_clients?, "New firm should have cached a no-clients response" + assert_equal 0, firm.clients_count, "New firm should have cached 0 clients count" + + assert !firm.clients(true).empty?, "New firm should have reloaded client objects" + assert firm.has_clients?(true), "New firm should have reloaded with a have-clients response" + assert_equal 1, firm.clients_count(true), "New firm should have reloaded clients count" + end + + def test_included_in_collection + assert companies(:first_firm).clients.include?(Client.find(2)) + end + + def test_build_to_collection + assert_equal 1, companies(:first_firm).clients_of_firm_count + new_client = companies(:first_firm).build_to_clients_of_firm("name" => "Another Client") + assert_equal "Another Client", new_client.name + assert new_client.save + + assert new_client.firm?(companies(:first_firm)) + assert_equal 2, companies(:first_firm).clients_of_firm_count(true) + end + + def test_create_in_collection + assert_equal companies(:first_firm).create_in_clients_of_firm("name" => "Another Client"), companies(:first_firm).clients_of_firm(true).last + end + + def test_has_and_belongs_to_many + david = Developer.find(1) + assert david.has_projects? + assert_equal 2, david.projects_count + + active_record = Project.find(1) + assert active_record.has_developers? + assert_equal 3, active_record.developers_count + assert active_record.developers.include?(david) + end + + def test_has_and_belongs_to_many_removing + david = Developer.find(1) + active_record = Project.find(1) + + david.remove_projects(active_record) + + assert_equal 1, david.projects_count + assert_equal 2, active_record.developers_count + end + + def test_has_and_belongs_to_many_zero + david = Developer.find(1) + david.remove_projects(Project.find_all) + + assert_equal 0, david.projects_count + assert !david.has_projects? + end + + def test_has_and_belongs_to_many_adding + jamis = Developer.find(2) + action_controller = Project.find(2) + + jamis.add_projects(action_controller) + + assert_equal 2, jamis.projects_count + assert_equal 2, action_controller.developers_count + end + + def test_has_and_belongs_to_many_adding_from_the_project + jamis = Developer.find(2) + action_controller = Project.find(2) + + action_controller.add_developers(jamis) + + assert_equal 2, jamis.projects_count + assert_equal 2, action_controller.developers_count + end + + def test_has_and_belongs_to_many_adding_a_collection + aredridel = Developer.new("name" => "Aredridel") + aredridel.save + + aredridel.add_projects([ Project.find(1), Project.find(2) ]) + assert_equal 2, aredridel.projects_count + end + + def test_belongs_to_counter + topic = Topic.create("title" => "Apple", "content" => "hello world") + assert_equal 0, topic.send(:read_attribute, "replies_count"), "No replies yet" + + reply = topic.create_in_replies("title" => "I'm saying no!", "content" => "over here") + assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count"), "First reply created" + + reply.destroy + assert_equal 0, Topic.find(topic.id).send(:read_attribute, "replies_count"), "First reply deleted" + end + + def test_natural_assignment_of_has_one + apple = Firm.create("name" => "Apple") + citibank = Account.create("credit_limit" => 10) + apple.account = citibank + assert_equal apple.id, citibank.firm_id + end + + def test_natural_assignment_of_belongs_to + apple = Firm.create("name" => "Apple") + citibank = Account.create("credit_limit" => 10) + citibank.firm = apple + assert_equal apple.id, citibank.firm_id + end + + def test_natural_assignment_of_has_many + apple = Firm.create("name" => "Apple") + natural = Client.create("name" => "Natural Company") + apple.clients << natural + assert_equal apple.id, natural.firm_id + assert_equal Client.find(natural.id), Firm.find(apple.id).clients.find(natural.id) + apple.clients.delete natural + assert_raises(ActiveRecord::RecordNotFound) { + Firm.find(apple.id).clients.find(natural.id) + } + end + + def test_natural_adding_of_has_and_belongs_to_many + rails = Project.create("name" => "Rails") + ap = Project.create("name" => "Action Pack") + john = Developer.create("name" => "John") + mike = Developer.create("name" => "Mike") + rails.developers << john + rails.developers << mike + + assert_equal Developer.find(john.id), Project.find(rails.id).developers.find(john.id) + assert_equal Developer.find(mike.id), Project.find(rails.id).developers.find(mike.id) + assert_equal Project.find(rails.id), Developer.find(mike.id).projects.find(rails.id) + assert_equal Project.find(rails.id), Developer.find(john.id).projects.find(rails.id) + ap.developers << john + assert_equal Developer.find(john.id), Project.find(ap.id).developers.find(john.id) + assert_equal Project.find(ap.id), Developer.find(john.id).projects.find(ap.id) + + ap.developers.delete john + assert_raises(ActiveRecord::RecordNotFound) { + Project.find(ap.id).developers.find(john.id) + } + assert_raises(ActiveRecord::RecordNotFound) { + Developer.find(john.id).projects.find(ap.id) + } + end + + def test_storing_in_pstore + require "pstore" + require "tmpdir" + apple = Firm.create("name" => "Apple") + natural = Client.new("name" => "Natural Company") + apple.clients << natural + + db = PStore.new(File.join(Dir.tmpdir, "ar-pstore-association-test")) + db.transaction do + db["apple"] = apple + end + + db = PStore.new(File.join(Dir.tmpdir, "ar-pstore-association-test")) + db.transaction do + assert_equal "Natural Company", db["apple"].clients.first.name + end + end + + def test_has_many_find_all + assert_equal 2, Firm.find_first.find_all_in_clients("#{QUOTED_TYPE} = 'Client'").length + assert_equal 1, Firm.find_first.find_all_in_clients("name = 'Summit'").length + end + + def test_has_one + assert companies(:first_firm).account?(Account.find(1)) + assert companies(:first_firm).has_account?, "37signals should have an account" + assert Account.find(1).firm?(companies(:first_firm)), "37signals account should be able to backtrack" + assert Account.find(1).has_firm?, "37signals account should be able to backtrack" + + assert !Account.find(2).has_firm?, "Unknown isn't linked" + assert !Account.find(2).firm?(companies(:first_firm)), "Unknown isn't linked" + end + + def test_has_one_build + firm = Firm.new("name" => "GlobalMegaCorp") + assert firm.save + + account = firm.build_account("credit_limit" => 1000) + assert account.save + assert_equal account, firm.account + end + + def test_has_one_failing_build_association + firm = Firm.new("name" => "GlobalMegaCorp") + firm.save + + account = firm.build_account + assert !account.save + assert_equal "can't be empty", account.errors.on("credit_limit") + end + + def test_has_one_create + firm = Firm.new("name" => "GlobalMegaCorp") + firm.save + assert_equal firm.create_account("credit_limit" => 1000), firm.account + end +end diff --git a/vendor/rails/activerecord/test/deprecated_finder_test.rb b/vendor/rails/activerecord/test/deprecated_finder_test.rb new file mode 100755 index 00000000..35c8a237 --- /dev/null +++ b/vendor/rails/activerecord/test/deprecated_finder_test.rb @@ -0,0 +1,134 @@ +require 'abstract_unit' +require 'fixtures/company' +require 'fixtures/topic' +require 'fixtures/entrant' +require 'fixtures/developer' + +class DeprecatedFinderTest < Test::Unit::TestCase + fixtures :companies, :topics, :entrants, :developers + + def test_find_all_with_limit + entrants = Entrant.find_all nil, "id ASC", 2 + + assert_equal(2, entrants.size) + assert_equal(entrants(:first).name, entrants.first.name) + end + + def test_find_all_with_prepared_limit_and_offset + entrants = Entrant.find_all nil, "id ASC", [2, 1] + + assert_equal(2, entrants.size) + assert_equal(entrants(:second).name, entrants.first.name) + end + + def test_find_first + first = Topic.find_first "title = 'The First Topic'" + assert_equal(topics(:first).title, first.title) + end + + def test_find_first_failing + first = Topic.find_first "title = 'The First Topic!'" + assert_nil(first) + end + + def test_deprecated_find_on_conditions + assert Topic.find_on_conditions(1, ["approved = ?", false]) + assert_raises(ActiveRecord::RecordNotFound) { Topic.find_on_conditions(1, ["approved = ?", true]) } + end + + def test_condition_interpolation + assert_kind_of Firm, Company.find_first(["name = '%s'", "37signals"]) + assert_nil Company.find_first(["name = '%s'", "37signals!"]) + assert_nil Company.find_first(["name = '%s'", "37signals!' OR 1=1"]) + assert_kind_of Time, Topic.find_first(["id = %d", 1]).written_on + end + + def test_bind_variables + assert_kind_of Firm, Company.find_first(["name = ?", "37signals"]) + assert_nil Company.find_first(["name = ?", "37signals!"]) + assert_nil Company.find_first(["name = ?", "37signals!' OR 1=1"]) + assert_kind_of Time, Topic.find_first(["id = ?", 1]).written_on + assert_raises(ActiveRecord::PreparedStatementInvalid) { + Company.find_first(["id=? AND name = ?", 2]) + } + assert_raises(ActiveRecord::PreparedStatementInvalid) { + Company.find_first(["id=?", 2, 3, 4]) + } + end + + def test_bind_variables_with_quotes + Company.create("name" => "37signals' go'es agains") + assert Company.find_first(["name = ?", "37signals' go'es agains"]) + end + + def test_named_bind_variables_with_quotes + Company.create("name" => "37signals' go'es agains") + assert Company.find_first(["name = :name", {:name => "37signals' go'es agains"}]) + end + + def test_named_bind_variables + assert_equal '1', bind(':a', :a => 1) # ' ruby-mode + assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode + + assert_kind_of Firm, Company.find_first(["name = :name", { :name => "37signals" }]) + assert_nil Company.find_first(["name = :name", { :name => "37signals!" }]) + assert_nil Company.find_first(["name = :name", { :name => "37signals!' OR 1=1" }]) + assert_kind_of Time, Topic.find_first(["id = :id", { :id => 1 }]).written_on + end + + def test_count + assert_equal(0, Entrant.count("id > 3")) + assert_equal(1, Entrant.count(["id > ?", 2])) + assert_equal(2, Entrant.count(["id > ?", 1])) + end + + def test_count_by_sql + assert_equal(0, Entrant.count_by_sql("SELECT COUNT(*) FROM entrants WHERE id > 3")) + assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2])) + assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1])) + end + + def test_find_all_with_limit + first_five_developers = Developer.find_all nil, 'id ASC', 5 + assert_equal 5, first_five_developers.length + assert_equal 'David', first_five_developers.first.name + assert_equal 'fixture_5', first_five_developers.last.name + + no_developers = Developer.find_all nil, 'id ASC', 0 + assert_equal 0, no_developers.length + + assert_equal first_five_developers, Developer.find_all(nil, 'id ASC', [5]) + assert_equal no_developers, Developer.find_all(nil, 'id ASC', [0]) + end + + def test_find_all_with_limit_and_offset + first_three_developers = Developer.find_all nil, 'id ASC', [3, 0] + second_three_developers = Developer.find_all nil, 'id ASC', [3, 3] + last_two_developers = Developer.find_all nil, 'id ASC', [2, 8] + + assert_equal 3, first_three_developers.length + assert_equal 3, second_three_developers.length + assert_equal 2, last_two_developers.length + + assert_equal 'David', first_three_developers.first.name + assert_equal 'fixture_4', second_three_developers.first.name + assert_equal 'fixture_9', last_two_developers.first.name + end + + def test_find_all_by_one_attribute_with_options + topics = Topic.find_all_by_content("Have a nice day", "id DESC") + assert topics(:first), topics.last + + topics = Topic.find_all_by_content("Have a nice day", "id DESC") + assert topics(:first), topics.first + end + + protected + def bind(statement, *vars) + if vars.first.is_a?(Hash) + ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first) + else + ActiveRecord::Base.send(:replace_bind_variables, statement, vars) + end + end +end diff --git a/vendor/rails/activerecord/test/finder_test.rb b/vendor/rails/activerecord/test/finder_test.rb new file mode 100644 index 00000000..98537eca --- /dev/null +++ b/vendor/rails/activerecord/test/finder_test.rb @@ -0,0 +1,381 @@ +require 'abstract_unit' +require 'fixtures/company' +require 'fixtures/topic' +require 'fixtures/reply' +require 'fixtures/entrant' +require 'fixtures/developer' +require 'fixtures/post' + +class FinderTest < Test::Unit::TestCase + fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :accounts + + def test_find + assert_equal(topics(:first).title, Topic.find(1).title) + end + + def test_exists + assert (Topic.exists?(1)) + assert !(Topic.exists?(45)) + assert !(Topic.exists?("foo")) + assert !(Topic.exists?([1,2])) + end + + def test_find_by_array_of_one_id + assert_kind_of(Array, Topic.find([ 1 ])) + assert_equal(1, Topic.find([ 1 ]).length) + end + + def test_find_by_ids + assert_equal(2, Topic.find(1, 2).length) + assert_equal(topics(:second).title, Topic.find([ 2 ]).first.title) + end + + def test_find_an_empty_array + assert_equal [], Topic.find([]) + end + + def test_find_by_ids_missing_one + assert_raises(ActiveRecord::RecordNotFound) { + Topic.find(1, 2, 45) + } + end + + def test_find_all_with_limit + entrants = Entrant.find(:all, :order => "id ASC", :limit => 2) + + assert_equal(2, entrants.size) + assert_equal(entrants(:first).name, entrants.first.name) + end + + def test_find_all_with_prepared_limit_and_offset + entrants = Entrant.find(:all, :order => "id ASC", :limit => 2, :offset => 1) + + assert_equal(2, entrants.size) + assert_equal(entrants(:second).name, entrants.first.name) + + entrants = Entrant.find(:all, :order => "id ASC", :limit => 2, :offset => 2) + assert_equal(1, entrants.size) + assert_equal(entrants(:third).name, entrants.first.name) + end + + def test_find_all_with_limit_and_offset_and_multiple_orderings + developers = Developer.find(:all, :order => "salary ASC, id DESC", :limit => 3, :offset => 1) + assert_equal ["David", "fixture_10", "fixture_9"], developers.collect {|d| d.name} + end + + def test_find_with_limit_and_condition + developers = Developer.find(:all, :order => "id DESC", :conditions => "salary = 100000", :limit => 3, :offset =>7) + assert_equal(1, developers.size) + assert_equal("fixture_3", developers.first.name) + end + + def test_find_with_entire_select_statement + topics = Topic.find_by_sql "SELECT * FROM topics WHERE author_name = 'Mary'" + + assert_equal(1, topics.size) + assert_equal(topics(:second).title, topics.first.title) + end + + def test_find_with_prepared_select_statement + topics = Topic.find_by_sql ["SELECT * FROM topics WHERE author_name = ?", "Mary"] + + assert_equal(1, topics.size) + assert_equal(topics(:second).title, topics.first.title) + end + + def test_find_first + first = Topic.find(:first, :conditions => "title = 'The First Topic'") + assert_equal(topics(:first).title, first.title) + end + + def test_find_first_failing + first = Topic.find(:first, :conditions => "title = 'The First Topic!'") + assert_nil(first) + end + + def test_unexisting_record_exception_handling + assert_raises(ActiveRecord::RecordNotFound) { + Topic.find(1).parent + } + + Topic.find(2).topic + end + + def test_find_only_some_columns + topic = Topic.find(1, :select => "author_name") + assert_raises(NoMethodError) { topic.title } + assert_equal "David", topic.author_name + assert !topic.attribute_present?("title") + assert !topic.respond_to?("title") + assert topic.attribute_present?("author_name") + assert topic.respond_to?("author_name") + end + + def test_find_on_conditions + assert Topic.find(1, :conditions => ["approved = ?", false]) + assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => ["approved = ?", true]) } + end + + def test_condition_interpolation + assert_kind_of Firm, Company.find(:first, :conditions => ["name = '%s'", "37signals"]) + assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!"]) + assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!' OR 1=1"]) + assert_kind_of Time, Topic.find(:first, :conditions => ["id = %d", 1]).written_on + end + + def test_bind_variables + assert_kind_of Firm, Company.find(:first, :conditions => ["name = ?", "37signals"]) + assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!"]) + assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!' OR 1=1"]) + assert_kind_of Time, Topic.find(:first, :conditions => ["id = ?", 1]).written_on + assert_raises(ActiveRecord::PreparedStatementInvalid) { + Company.find(:first, :conditions => ["id=? AND name = ?", 2]) + } + assert_raises(ActiveRecord::PreparedStatementInvalid) { + Company.find(:first, :conditions => ["id=?", 2, 3, 4]) + } + end + + def test_bind_variables_with_quotes + Company.create("name" => "37signals' go'es agains") + assert Company.find(:first, :conditions => ["name = ?", "37signals' go'es agains"]) + end + + def test_named_bind_variables_with_quotes + Company.create("name" => "37signals' go'es agains") + assert Company.find(:first, :conditions => ["name = :name", {:name => "37signals' go'es agains"}]) + end + + def test_bind_arity + assert_nothing_raised { bind '' } + assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '', 1 } + + assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '?' } + assert_nothing_raised { bind '?', 1 } + assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 } + end + + def test_named_bind_variables + assert_equal '1', bind(':a', :a => 1) # ' ruby-mode + assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode + + assert_kind_of Firm, Company.find(:first, :conditions => ["name = :name", { :name => "37signals" }]) + assert_nil Company.find(:first, :conditions => ["name = :name", { :name => "37signals!" }]) + assert_nil Company.find(:first, :conditions => ["name = :name", { :name => "37signals!' OR 1=1" }]) + assert_kind_of Time, Topic.find(:first, :conditions => ["id = :id", { :id => 1 }]).written_on + end + + def test_bind_enumerable + assert_equal '1,2,3', bind('?', [1, 2, 3]) + assert_equal %('a','b','c'), bind('?', %w(a b c)) + + assert_equal '1,2,3', bind(':a', :a => [1, 2, 3]) + assert_equal %('a','b','c'), bind(':a', :a => %w(a b c)) # ' + + require 'set' + assert_equal '1,2,3', bind('?', Set.new([1, 2, 3])) + assert_equal %('a','b','c'), bind('?', Set.new(%w(a b c))) + + assert_equal '1,2,3', bind(':a', :a => Set.new([1, 2, 3])) + assert_equal %('a','b','c'), bind(':a', :a => Set.new(%w(a b c))) # ' + end + + def test_bind_string + assert_equal "''", bind('?', '') + end + + def test_string_sanitation + assert_not_equal "'something ' 1=1'", ActiveRecord::Base.sanitize("something ' 1=1") + assert_equal "'something; select table'", ActiveRecord::Base.sanitize("something; select table") + end + + def test_count + assert_equal(0, Entrant.count("id > 3")) + assert_equal(1, Entrant.count(["id > ?", 2])) + assert_equal(2, Entrant.count(["id > ?", 1])) + end + + def test_count_by_sql + assert_equal(0, Entrant.count_by_sql("SELECT COUNT(*) FROM entrants WHERE id > 3")) + assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2])) + assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1])) + end + + def test_find_by_one_attribute + assert_equal topics(:first), Topic.find_by_title("The First Topic") + assert_nil Topic.find_by_title("The First Topic!") + end + + def test_find_by_one_attribute_with_order_option + assert_equal accounts(:signals37), Account.find_by_credit_limit(50, :order => 'id') + assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :order => 'id DESC') + end + + def test_find_by_one_attribute_with_conditions + assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6]) + end + + def test_find_by_one_attribute_with_several_options + assert_equal accounts(:unknown), Account.find_by_credit_limit(50, :order => 'id DESC', :conditions => ['id != ?', 3]) + end + + def test_find_by_one_missing_attribute + assert_raises(NoMethodError) { Topic.find_by_undertitle("The First Topic!") } + end + + def test_find_by_two_attributes + assert_equal topics(:first), Topic.find_by_title_and_author_name("The First Topic", "David") + assert_nil Topic.find_by_title_and_author_name("The First Topic", "Mary") + end + + def test_find_all_by_one_attribute + topics = Topic.find_all_by_content("Have a nice day") + assert_equal 2, topics.size + assert topics.include?(topics(:first)) + + assert_equal [], Topic.find_all_by_title("The First Topic!!") + end + + def test_find_all_by_one_attribute_with_options + topics = Topic.find_all_by_content("Have a nice day", :order => "id DESC") + assert topics(:first), topics.last + + topics = Topic.find_all_by_content("Have a nice day", :order => "id") + assert topics(:first), topics.first + end + + def test_find_all_by_array_attribute + assert_equal 2, Topic.find_all_by_title(["The First Topic", "The Second Topic's of the day"]).size + end + + def test_find_all_by_boolean_attribute + topics = Topic.find_all_by_approved(false) + assert_equal 1, topics.size + assert topics.include?(topics(:first)) + + topics = Topic.find_all_by_approved(true) + assert_equal 1, topics.size + assert topics.include?(topics(:second)) + end + + def test_find_by_nil_attribute + topic = Topic.find_by_last_read nil + assert_not_nil topic + assert_nil topic.last_read + end + + def test_find_all_by_nil_attribute + topics = Topic.find_all_by_last_read nil + assert_equal 1, topics.size + assert_nil topics[0].last_read + end + + def test_find_by_nil_and_not_nil_attributes + topic = Topic.find_by_last_read_and_author_name nil, "Mary" + assert_equal "Mary", topic.author_name + end + + def test_find_all_by_nil_and_not_nil_attributes + topics = Topic.find_all_by_last_read_and_author_name nil, "Mary" + assert_equal 1, topics.size + assert_equal "Mary", topics[0].author_name + end + + def test_find_or_create_from_one_attribute + number_of_companies = Company.count + sig38 = Company.find_or_create_by_name("38signals") + assert_equal number_of_companies + 1, Company.count + assert_equal sig38, Company.find_or_create_by_name("38signals") + end + + def test_find_or_create_from_two_attributes + number_of_topics = Topic.count + another = Topic.find_or_create_by_title_and_author_name("Another topic","John") + assert_equal number_of_topics + 1, Topic.count + assert_equal another, Topic.find_or_create_by_title_and_author_name("Another topic", "John") + end + + def test_find_with_bad_sql + assert_raises(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" } + end + + def test_find_with_invalid_params + assert_raises(ArgumentError) { Topic.find :first, :join => "It should be `joins'" } + assert_raises(ArgumentError) { Topic.find :first, :conditions => '1 = 1', :join => "It should be `joins'" } + end + + def test_find_all_with_limit + first_five_developers = Developer.find :all, :order => 'id ASC', :limit => 5 + assert_equal 5, first_five_developers.length + assert_equal 'David', first_five_developers.first.name + assert_equal 'fixture_5', first_five_developers.last.name + + no_developers = Developer.find :all, :order => 'id ASC', :limit => 0 + assert_equal 0, no_developers.length + end + + def test_find_all_with_limit_and_offset + first_three_developers = Developer.find :all, :order => 'id ASC', :limit => 3, :offset => 0 + second_three_developers = Developer.find :all, :order => 'id ASC', :limit => 3, :offset => 3 + last_two_developers = Developer.find :all, :order => 'id ASC', :limit => 2, :offset => 8 + + assert_equal 3, first_three_developers.length + assert_equal 3, second_three_developers.length + assert_equal 2, last_two_developers.length + + assert_equal 'David', first_three_developers.first.name + assert_equal 'fixture_4', second_three_developers.first.name + assert_equal 'fixture_9', last_two_developers.first.name + end + + def test_find_all_with_limit_and_offset_and_multiple_order_clauses + first_three_posts = Post.find :all, :order => 'author_id, id', :limit => 3, :offset => 0 + second_three_posts = Post.find :all, :order => ' author_id,id ', :limit => 3, :offset => 3 + last_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 6 + + assert_equal [[0,3],[1,1],[1,2]], first_three_posts.map { |p| [p.author_id, p.id] } + assert_equal [[1,4],[1,5],[1,6]], second_three_posts.map { |p| [p.author_id, p.id] } + assert_equal [[2,7]], last_posts.map { |p| [p.author_id, p.id] } + end + + def test_find_all_with_join + developers_on_project_one = Developer.find( + :all, + :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', + :conditions => 'project_id=1' + ) + assert_equal 3, developers_on_project_one.length + developer_names = developers_on_project_one.map { |d| d.name } + assert developer_names.include?('David') + assert developer_names.include?('Jamis') + end + + def test_find_by_id_with_conditions_with_or + assert_nothing_raised do + Post.find([1,2,3], + :conditions => "posts.id <= 3 OR posts.#{QUOTED_TYPE} = 'Post'") + end + end + + def test_select_value + assert_equal "37signals", Company.connection.select_value("SELECT name FROM companies WHERE id = 1") + assert_nil Company.connection.select_value("SELECT name FROM companies WHERE id = -1") + # make sure we didn't break count... + assert_equal 0, Company.count_by_sql("SELECT COUNT(*) FROM companies WHERE name = 'Halliburton'") + assert_equal 1, Company.count_by_sql("SELECT COUNT(*) FROM companies WHERE name = '37signals'") + end + + def test_select_values + assert_equal ["1","2","3","4","5","6","7","8"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map! { |i| i.to_s } + assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel"], Company.connection.select_values("SELECT name FROM companies ORDER BY id") + end + + protected + def bind(statement, *vars) + if vars.first.is_a?(Hash) + ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first) + else + ActiveRecord::Base.send(:replace_bind_variables, statement, vars) + end + end +end diff --git a/vendor/rails/activerecord/test/fixtures/accounts.yml b/vendor/rails/activerecord/test/fixtures/accounts.yml new file mode 100644 index 00000000..a3d6742d --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/accounts.yml @@ -0,0 +1,23 @@ +signals37: + id: 1 + firm_id: 1 + credit_limit: 50 + +unknown: + id: 2 + credit_limit: 50 + +rails_core_account: + id: 3 + firm_id: 6 + credit_limit: 50 + +last_account: + id: 4 + firm_id: 2 + credit_limit: 60 + +rails_core_account_2: + id: 5 + firm_id: 6 + credit_limit: 55 \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/author.rb b/vendor/rails/activerecord/test/fixtures/author.rb new file mode 100644 index 00000000..99142f66 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/author.rb @@ -0,0 +1,76 @@ +class Author < ActiveRecord::Base + has_many :posts + has_many :posts_with_comments, :include => :comments, :class_name => "Post" + has_many :posts_with_categories, :include => :categories, :class_name => "Post" + has_many :posts_with_comments_and_categories, :include => [ :comments, :categories ], :order => "posts.id", :class_name => "Post" + has_many :comments, :through => :posts + has_many :funky_comments, :through => :posts, :source => :comments + + has_many :special_posts, :class_name => "Post" + has_many :hello_posts, :class_name => "Post", :conditions=>"\#{aliased_table_name}.body = 'hello'" + has_many :nonexistent_posts, :class_name => "Post", :conditions=>"\#{aliased_table_name}.body = 'nonexistent'" + has_many :posts_with_callbacks, :class_name => "Post", :before_add => :log_before_adding, + :after_add => :log_after_adding, + :before_remove => :log_before_removing, + :after_remove => :log_after_removing + has_many :posts_with_proc_callbacks, :class_name => "Post", + :before_add => Proc.new {|o, r| o.post_log << "before_adding#{r.id}"}, + :after_add => Proc.new {|o, r| o.post_log << "after_adding#{r.id}"}, + :before_remove => Proc.new {|o, r| o.post_log << "before_removing#{r.id}"}, + :after_remove => Proc.new {|o, r| o.post_log << "after_removing#{r.id}"} + has_many :posts_with_multiple_callbacks, :class_name => "Post", + :before_add => [:log_before_adding, Proc.new {|o, r| o.post_log << "before_adding_proc#{r.id}"}], + :after_add => [:log_after_adding, Proc.new {|o, r| o.post_log << "after_adding_proc#{r.id}"}] + has_many :unchangable_posts, :class_name => "Post", :before_add => :raise_exception, :after_add => :log_after_adding + + has_many :categorizations + has_many :categories, :through => :categorizations + + has_many :nothings, :through => :kateggorisatons, :class_name => 'Category' + + has_many :author_favorites + has_many :favorite_authors, :through => :author_favorites, :order => 'name' + + has_many :tagging, :through => :posts # through polymorphic has_one + has_many :taggings, :through => :posts, :source => :taggings # through polymorphic has_many + has_many :tags, :through => :posts # through has_many :through + has_many :post_categories, :through => :posts, :source => :categories + + belongs_to :author_address + + attr_accessor :post_log + + def after_initialize + @post_log = [] + end + + private + def log_before_adding(object) + @post_log << "before_adding#{object.id}" + end + + def log_after_adding(object) + @post_log << "after_adding#{object.id}" + end + + def log_before_removing(object) + @post_log << "before_removing#{object.id}" + end + + def log_after_removing(object) + @post_log << "after_removing#{object.id}" + end + + def raise_exception(object) + raise Exception.new("You can't add a post") + end +end + +class AuthorAddress < ActiveRecord::Base + has_one :author +end + +class AuthorFavorite < ActiveRecord::Base + belongs_to :author + belongs_to :favorite_author, :class_name => "Author", :foreign_key => 'favorite_author_id' +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/author_favorites.yml b/vendor/rails/activerecord/test/fixtures/author_favorites.yml new file mode 100644 index 00000000..e81fdac7 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/author_favorites.yml @@ -0,0 +1,4 @@ +david_mary: + id: 1 + author_id: 1 + favorite_author_id: 2 \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/authors.yml b/vendor/rails/activerecord/test/fixtures/authors.yml new file mode 100644 index 00000000..f59b84fa --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/authors.yml @@ -0,0 +1,7 @@ +david: + id: 1 + name: David + +mary: + id: 2 + name: Mary diff --git a/vendor/rails/activerecord/test/fixtures/auto_id.rb b/vendor/rails/activerecord/test/fixtures/auto_id.rb new file mode 100644 index 00000000..d720e2be --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/auto_id.rb @@ -0,0 +1,4 @@ +class AutoId < ActiveRecord::Base + def self.table_name () "auto_id_tests" end + def self.primary_key () "auto_id" end +end diff --git a/vendor/rails/activerecord/test/fixtures/bad_fixtures/attr_with_numeric_first_char b/vendor/rails/activerecord/test/fixtures/bad_fixtures/attr_with_numeric_first_char new file mode 100644 index 00000000..ef27947f --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/bad_fixtures/attr_with_numeric_first_char @@ -0,0 +1 @@ +1b => 1 diff --git a/vendor/rails/activerecord/test/fixtures/bad_fixtures/attr_with_spaces b/vendor/rails/activerecord/test/fixtures/bad_fixtures/attr_with_spaces new file mode 100644 index 00000000..46fd6f2f --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/bad_fixtures/attr_with_spaces @@ -0,0 +1 @@ +a b => 1 diff --git a/vendor/rails/activerecord/test/fixtures/bad_fixtures/blank_line b/vendor/rails/activerecord/test/fixtures/bad_fixtures/blank_line new file mode 100644 index 00000000..3ea1f717 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/bad_fixtures/blank_line @@ -0,0 +1,3 @@ +a => 1 + +b => 2 diff --git a/vendor/rails/activerecord/test/fixtures/bad_fixtures/duplicate_attributes b/vendor/rails/activerecord/test/fixtures/bad_fixtures/duplicate_attributes new file mode 100644 index 00000000..cc0236f2 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/bad_fixtures/duplicate_attributes @@ -0,0 +1,3 @@ +a => 1 +b => 2 +a => 3 diff --git a/vendor/rails/activerecord/test/fixtures/bad_fixtures/missing_value b/vendor/rails/activerecord/test/fixtures/bad_fixtures/missing_value new file mode 100644 index 00000000..fb59ec33 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/bad_fixtures/missing_value @@ -0,0 +1 @@ +a => diff --git a/vendor/rails/activerecord/test/fixtures/binary.rb b/vendor/rails/activerecord/test/fixtures/binary.rb new file mode 100644 index 00000000..950c4591 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/binary.rb @@ -0,0 +1,2 @@ +class Binary < ActiveRecord::Base +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/categories.yml b/vendor/rails/activerecord/test/fixtures/categories.yml new file mode 100644 index 00000000..b0770a09 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/categories.yml @@ -0,0 +1,14 @@ +general: + id: 1 + name: General + type: Category + +technology: + id: 2 + name: Technology + type: Category + +sti_test: + id: 3 + name: Special category + type: SpecialCategory diff --git a/vendor/rails/activerecord/test/fixtures/categories/special_categories.yml b/vendor/rails/activerecord/test/fixtures/categories/special_categories.yml new file mode 100644 index 00000000..517fc8f7 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/categories/special_categories.yml @@ -0,0 +1,9 @@ +sub_special_1: + id: 100 + name: A special category in a subdir file + type: SpecialCategory + +sub_special_2: + id: 101 + name: Another special category + type: SpecialCategory diff --git a/vendor/rails/activerecord/test/fixtures/categories/subsubdir/arbitrary_filename.yml b/vendor/rails/activerecord/test/fixtures/categories/subsubdir/arbitrary_filename.yml new file mode 100644 index 00000000..389a04a5 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/categories/subsubdir/arbitrary_filename.yml @@ -0,0 +1,4 @@ +sub_special_3: + id: 102 + name: A special category in an arbitrarily named subsubdir file + type: SpecialCategory diff --git a/vendor/rails/activerecord/test/fixtures/categories_ordered.yml b/vendor/rails/activerecord/test/fixtures/categories_ordered.yml new file mode 100644 index 00000000..294a6368 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/categories_ordered.yml @@ -0,0 +1,7 @@ +--- !omap +<% 100.times do |i| %> +- fixture_no_<%= i %>: + id: <%= i %> + name: <%= "Category #{i}" %> + type: Category +<% end %> diff --git a/vendor/rails/activerecord/test/fixtures/categories_posts.yml b/vendor/rails/activerecord/test/fixtures/categories_posts.yml new file mode 100644 index 00000000..9b67ab4f --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/categories_posts.yml @@ -0,0 +1,23 @@ +general_welcome: + category_id: 1 + post_id: 1 + +technology_welcome: + category_id: 2 + post_id: 1 + +general_thinking: + category_id: 1 + post_id: 2 + +general_sti_habtm: + category_id: 1 + post_id: 6 + +sti_test_sti_habtm: + category_id: 3 + post_id: 6 + +general_hello: + category_id: 1 + post_id: 4 diff --git a/vendor/rails/activerecord/test/fixtures/categorization.rb b/vendor/rails/activerecord/test/fixtures/categorization.rb new file mode 100644 index 00000000..10594323 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/categorization.rb @@ -0,0 +1,5 @@ +class Categorization < ActiveRecord::Base + belongs_to :post + belongs_to :category + belongs_to :author +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/categorizations.yml b/vendor/rails/activerecord/test/fixtures/categorizations.yml new file mode 100644 index 00000000..f8701fbd --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/categorizations.yml @@ -0,0 +1,11 @@ +david_welcome_general: + id: 1 + author_id: 1 + post_id: 1 + category_id: 1 + +mary_thinking_sti: + id: 2 + author_id: 2 + post_id: 2 + category_id: 3 \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/category.rb b/vendor/rails/activerecord/test/fixtures/category.rb new file mode 100644 index 00000000..6917c51d --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/category.rb @@ -0,0 +1,21 @@ +class Category < ActiveRecord::Base + has_and_belongs_to_many :posts + has_and_belongs_to_many :special_posts, :class_name => "Post" + has_and_belongs_to_many :hello_posts, :class_name => "Post", :conditions => "\#{aliased_table_name}.body = 'hello'" + has_and_belongs_to_many :nonexistent_posts, :class_name => "Post", :conditions=>"\#{aliased_table_name}.body = 'nonexistent'" + + def self.what_are_you + 'a category...' + end + + has_many :categorizations + has_many :authors, :through => :categorizations, :select => 'authors.*, categorizations.post_id' +end + +class SpecialCategory < Category + + def self.what_are_you + 'a special category...' + end + +end diff --git a/vendor/rails/activerecord/test/fixtures/column_name.rb b/vendor/rails/activerecord/test/fixtures/column_name.rb new file mode 100644 index 00000000..ec07205a --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/column_name.rb @@ -0,0 +1,3 @@ +class ColumnName < ActiveRecord::Base + def self.table_name () "colnametests" end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/comment.rb b/vendor/rails/activerecord/test/fixtures/comment.rb new file mode 100644 index 00000000..3eab263f --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/comment.rb @@ -0,0 +1,23 @@ +class Comment < ActiveRecord::Base + belongs_to :post + + def self.what_are_you + 'a comment...' + end + + def self.search_by_type(q) + self.find(:all, :conditions => ["#{QUOTED_TYPE} = ?", q]) + end +end + +class SpecialComment < Comment + def self.what_are_you + 'a special comment...' + end +end + +class VerySpecialComment < Comment + def self.what_are_you + 'a very special comment...' + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/comments.yml b/vendor/rails/activerecord/test/fixtures/comments.yml new file mode 100644 index 00000000..758eaf6d --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/comments.yml @@ -0,0 +1,65 @@ +greetings: + id: 1 + post_id: 1 + body: Thank you for the welcome + type: Comment + +more_greetings: + id: 2 + post_id: 1 + body: Thank you again for the welcome + type: Comment + +does_it_hurt: + id: 3 + post_id: 2 + body: Don't think too hard + type: SpecialComment + +eager_sti_on_associations_comment: + id: 4 + post_id: 4 + body: Normal type + type: Comment + +eager_sti_on_associations_vs_comment: + id: 5 + post_id: 4 + body: Very Special type + type: VerySpecialComment + +eager_sti_on_associations_s_comment1: + id: 6 + post_id: 4 + body: Special type + type: SpecialComment + +eager_sti_on_associations_s_comment2: + id: 7 + post_id: 4 + body: Special type 2 + type: SpecialComment + +eager_sti_on_associations_comment: + id: 8 + post_id: 4 + body: Normal type + type: Comment + +check_eager_sti_on_associations: + id: 9 + post_id: 5 + body: Normal type + type: Comment + +check_eager_sti_on_associations2: + id: 10 + post_id: 5 + body: Special Type + type: SpecialComment + +eager_other_comment1: + id: 11 + post_id: 7 + body: go crazy + type: SpecialComment diff --git a/vendor/rails/activerecord/test/fixtures/companies.yml b/vendor/rails/activerecord/test/fixtures/companies.yml new file mode 100644 index 00000000..f2e638d3 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/companies.yml @@ -0,0 +1,50 @@ +first_client: + id: 2 + type: Client + firm_id: 1 + client_of: 2 + name: Summit + ruby_type: Client + +first_firm: + id: 1 + type: Firm + name: 37signals + ruby_type: Firm + +second_client: + id: 3 + type: Client + firm_id: 1 + client_of: 1 + name: Microsoft + ruby_type: Client + +another_firm: + id: 4 + type: Firm + name: Flamboyant Software + ruby_type: Firm + +another_client: + id: 5 + type: Client + firm_id: 4 + client_of: 4 + name: Ex Nihilo + ruby_type: Client + +rails_core: + id: 6 + name: RailsCore + type: DependentFirm + +leetsoft: + id: 7 + name: Leetsoft + client_of: 6 + +jadedpixel: + id: 8 + name: Jadedpixel + client_of: 6 \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/company.rb b/vendor/rails/activerecord/test/fixtures/company.rb new file mode 100755 index 00000000..59a7cdd1 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/company.rb @@ -0,0 +1,85 @@ +class Company < ActiveRecord::Base + attr_protected :rating + set_sequence_name :companies_nonstd_seq + + validates_presence_of :name + + has_one :dummy_account, :foreign_key => "firm_id", :class_name => "Account" +end + + +class Firm < Company + has_many :clients, :order => "id", :dependent => :destroy, :counter_sql => + "SELECT COUNT(*) FROM companies WHERE firm_id = 1 " + + "AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )" + has_many :clients_sorted_desc, :class_name => "Client", :order => "id DESC" + has_many :clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id" + has_many :dependent_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :destroy + has_many :exclusively_dependent_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all + has_many :limited_clients, :class_name => "Client", :order => "id", :limit => 1 + has_many :clients_like_ms, :conditions => "name = 'Microsoft'", :class_name => "Client", :order => "id" + has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}' + has_many :clients_using_counter_sql, :class_name => "Client", + :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}', + :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = #{id}' + has_many :clients_using_zero_counter_sql, :class_name => "Client", + :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}', + :counter_sql => 'SELECT 0 FROM companies WHERE client_of = #{id}' + has_many :no_clients_using_counter_sql, :class_name => "Client", + :finder_sql => 'SELECT * FROM companies WHERE client_of = 1000', + :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = 1000' + + has_one :account, :foreign_key => "firm_id", :dependent => :destroy +end + +class DependentFirm < Company + has_one :account, :foreign_key => "firm_id", :dependent => :nullify + has_many :companies, :foreign_key => 'client_of', :order => "id", :dependent => :nullify +end + + +class Client < Company + belongs_to :firm, :foreign_key => "client_of" + belongs_to :firm_with_basic_id, :class_name => "Firm", :foreign_key => "firm_id" + belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of" + belongs_to :firm_with_condition, :class_name => "Firm", :foreign_key => "client_of", :conditions => ["1 = ?", 1] + + # Record destruction so we can test whether firm.clients.clear has + # is calling client.destroy, deleting from the database, or setting + # foreign keys to NULL. + def self.destroyed_client_ids + @destroyed_client_ids ||= Hash.new { |h,k| h[k] = [] } + end + + before_destroy do |client| + if client.firm + Client.destroyed_client_ids[client.firm.id] << client.id + end + true + end + + # Used to test that read and question methods are not generated for these attributes + def ruby_type + read_attribute :ruby_type + end + + def rating? + query_attribute :rating + end +end + + +class SpecialClient < Client +end + +class VerySpecialClient < SpecialClient +end + +class Account < ActiveRecord::Base + belongs_to :firm + + protected + def validate + errors.add_on_empty "credit_limit" + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/company_in_module.rb b/vendor/rails/activerecord/test/fixtures/company_in_module.rb new file mode 100644 index 00000000..7372134a --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/company_in_module.rb @@ -0,0 +1,61 @@ +module MyApplication + module Business + class Company < ActiveRecord::Base + attr_protected :rating + end + + class Firm < Company + has_many :clients, :order => "id", :dependent => :destroy + has_many :clients_sorted_desc, :class_name => "Client", :order => "id DESC" + has_many :clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id" + has_many :clients_like_ms, :conditions => "name = 'Microsoft'", :class_name => "Client", :order => "id" + has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}' + + has_one :account, :dependent => :destroy + end + + class Client < Company + belongs_to :firm, :foreign_key => "client_of" + belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of" + end + + class Developer < ActiveRecord::Base + has_and_belongs_to_many :projects + + protected + def validate + errors.add_on_boundary_breaking("name", 3..20) + end + end + + class Project < ActiveRecord::Base + has_and_belongs_to_many :developers + end + + end + + module Billing + class Firm < ActiveRecord::Base + self.table_name = 'companies' + end + + module Nested + class Firm < ActiveRecord::Base + self.table_name = 'companies' + end + end + + class Account < ActiveRecord::Base + belongs_to :firm, :class_name => 'MyApplication::Business::Firm' + belongs_to :qualified_billing_firm, :class_name => 'MyApplication::Billing::Firm' + belongs_to :unqualified_billing_firm, :class_name => 'Firm' + belongs_to :nested_qualified_billing_firm, :class_name => 'MyApplication::Billing::Nested::Firm' + belongs_to :nested_unqualified_billing_firm, :class_name => 'Nested::Firm' + + protected + def validate + errors.add_on_empty "credit_limit" + end + end + end +end diff --git a/vendor/rails/activerecord/test/fixtures/computer.rb b/vendor/rails/activerecord/test/fixtures/computer.rb new file mode 100644 index 00000000..cc8deb1b --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/computer.rb @@ -0,0 +1,3 @@ +class Computer < ActiveRecord::Base + belongs_to :developer, :foreign_key=>'developer' +end diff --git a/vendor/rails/activerecord/test/fixtures/computers.yml b/vendor/rails/activerecord/test/fixtures/computers.yml new file mode 100644 index 00000000..daf969d7 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/computers.yml @@ -0,0 +1,4 @@ +workstation: + id: 1 + developer: 1 + extendedWarranty: 1 diff --git a/vendor/rails/activerecord/test/fixtures/course.rb b/vendor/rails/activerecord/test/fixtures/course.rb new file mode 100644 index 00000000..8a40fa74 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/course.rb @@ -0,0 +1,3 @@ +class Course < ActiveRecord::Base + has_many :entrants +end diff --git a/vendor/rails/activerecord/test/fixtures/courses.yml b/vendor/rails/activerecord/test/fixtures/courses.yml new file mode 100644 index 00000000..5ee19160 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/courses.yml @@ -0,0 +1,7 @@ +ruby: + id: 1 + name: Ruby Development + +java: + id: 2 + name: Java Development diff --git a/vendor/rails/activerecord/test/fixtures/customer.rb b/vendor/rails/activerecord/test/fixtures/customer.rb new file mode 100644 index 00000000..e23fd03a --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/customer.rb @@ -0,0 +1,55 @@ +class Customer < ActiveRecord::Base + composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] + composed_of :balance, :class_name => "Money", :mapping => %w(balance amount) + composed_of :gps_location +end + +class Address + attr_reader :street, :city, :country + + def initialize(street, city, country) + @street, @city, @country = street, city, country + end + + def close_to?(other_address) + city == other_address.city && country == other_address.country + end + + def ==(other) + other.is_a?(self.class) && other.street == street && other.city == city && other.country == country + end +end + +class Money + attr_reader :amount, :currency + + EXCHANGE_RATES = { "USD_TO_DKK" => 6, "DKK_TO_USD" => 0.6 } + + def initialize(amount, currency = "USD") + @amount, @currency = amount, currency + end + + def exchange_to(other_currency) + Money.new((amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor, other_currency) + end +end + +class GpsLocation + attr_reader :gps_location + + def initialize(gps_location) + @gps_location = gps_location + end + + def latitude + gps_location.split("x").first + end + + def longitude + gps_location.split("x").last + end + + def ==(other) + self.latitude == other.latitude && self.longitude == other.longitude + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/customers.yml b/vendor/rails/activerecord/test/fixtures/customers.yml new file mode 100644 index 00000000..9169d7d4 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/customers.yml @@ -0,0 +1,8 @@ +david: + id: 1 + name: David + balance: 50 + address_street: Funny Street + address_city: Scary Town + address_country: Loony Land + gps_location: 35.544623640962634x-105.9309951055148 diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/db2.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/db2.drop.sql new file mode 100644 index 00000000..c5b32bb9 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/db2.drop.sql @@ -0,0 +1,30 @@ +DROP TABLE accounts; +DROP TABLE funny_jokes; +DROP TABLE companies; +DROP TABLE topics; +DROP TABLE developers; +DROP TABLE projects; +DROP TABLE developers_projects; +DROP TABLE orders; +DROP TABLE customers; +DROP TABLE movies; +DROP TABLE subscribers; +DROP TABLE booleantests; +DROP TABLE auto_id_tests; +DROP TABLE entrants; +DROP TABLE colnametests; +DROP TABLE mixins; +DROP TABLE people; +DROP TABLE readers; +DROP TABLE binaries; +DROP TABLE computers; +DROP TABLE posts; +DROP TABLE comments; +DROP TABLE authors; +DROP TABLE tasks; +DROP TABLE categories; +DROP TABLE categories_posts; +DROP TABLE fk_test_has_pk; +DROP TABLE fk_test_has_fk; +DROP TABLE keyboards; +DROP TABLE legacy_things; diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/db2.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/db2.sql new file mode 100644 index 00000000..2f67e9ce --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/db2.sql @@ -0,0 +1,217 @@ +CREATE TABLE accounts ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + firm_id INT DEFAULT NULL, + credit_limit INT DEFAULT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE funny_jokes ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + name VARCHAR(50) DEFAULT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE companies ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + type VARCHAR(50) DEFAULT NULL, + ruby_type VARCHAR(50) DEFAULT NULL, + firm_id INT DEFAULT NULL, + name VARCHAR(50) DEFAULT NULL, + client_of INT DEFAULT NULL, + rating INT DEFAULT 1, + PRIMARY KEY (id) +); + +CREATE TABLE topics ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + title VARCHAR(255) DEFAULT NULL, + author_name VARCHAR(255) DEFAULT NULL, + author_email_address VARCHAR(255) DEFAULT NULL, + written_on TIMESTAMP DEFAULT NULL, + bonus_time TIME DEFAULT NULL, + last_read DATE DEFAULT NULL, + content VARCHAR(3000), + approved SMALLINT DEFAULT 1, + replies_count INT DEFAULT 0, + parent_id INT DEFAULT NULL, + type VARCHAR(50) DEFAULT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE developers ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + name VARCHAR(100) DEFAULT NULL, + salary INT DEFAULT 70000, + created_at TIMESTAMP DEFAULT NULL, + updated_at TIMESTAMP DEFAULT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE projects ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + name VARCHAR(100) DEFAULT NULL, + type VARCHAR(255) DEFAULT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE developers_projects ( + developer_id INT NOT NULL, + project_id INT NOT NULL, + joined_on DATE DEFAULT NULL, + access_level SMALLINT DEFAULT 1 +); + +CREATE TABLE orders ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + name VARCHAR(100) DEFAULT NULL, + billing_customer_id INT DEFAULT NULL, + shipping_customer_id INT DEFAULT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE customers ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + name VARCHAR(100) DEFAULT NULL, + balance INT DEFAULT 0, + address_street VARCHAR(100) DEFAULT NULL, + address_city VARCHAR(100) DEFAULT NULL, + address_country VARCHAR(100) DEFAULT NULL, + gps_location VARCHAR(100) DEFAULT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE movies ( + movieid INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + name VARCHAR(100) DEFAULT NULL, + PRIMARY KEY (movieid) +); + +CREATE TABLE subscribers ( + nick VARCHAR(100) NOT NULL, + name VARCHAR(100) DEFAULT NULL, + PRIMARY KEY (nick) +); + +CREATE TABLE booleantests ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + value INT DEFAULT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE auto_id_tests ( + auto_id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + value INT DEFAULT NULL, + PRIMARY KEY (auto_id) +); + +CREATE TABLE entrants ( + id INT NOT NULL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + course_id INT NOT NULL +); + +CREATE TABLE colnametests ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + references INT NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE mixins ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + parent_id INT DEFAULT NULL, + pos INT DEFAULT NULL, + created_at TIMESTAMP DEFAULT NULL, + updated_at TIMESTAMP DEFAULT NULL, + lft INT DEFAULT NULL, + rgt INT DEFAULT NULL, + root_id INT DEFAULT NULL, + type VARCHAR(40) DEFAULT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE people ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + first_name VARCHAR(40) NOT NULL, + lock_version INT DEFAULT 0, + PRIMARY KEY (id) +); + +CREATE TABLE readers ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + post_id INT NOT NULL, + person_id INT NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE binaries ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + data BLOB(50000), + PRIMARY KEY (id) +); + +CREATE TABLE computers ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + developer INT NOT NULL, + extendedWarranty INT NOT NULL +); + +CREATE TABLE posts ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + author_id INT DEFAULT NULL, + title VARCHAR(255) DEFAULT NULL, + type VARCHAR(255) DEFAULT NULL, + body VARCHAR(3000) DEFAULT NULL +); + +CREATE TABLE comments ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + post_id INT DEFAULT NULL, + type VARCHAR(255) DEFAULT NULL, + body VARCHAR(3000) DEFAULT NULL +); + +CREATE TABLE authors ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + name VARCHAR(255) DEFAULT NULL +); + +CREATE TABLE tasks ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + starting TIMESTAMP DEFAULT NULL, + ending TIMESTAMP DEFAULT NULL +); + +CREATE TABLE categories ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + name VARCHAR(255) NOT NULL, + type VARCHAR(40) DEFAULT NULL +); + +CREATE TABLE categories_posts ( + category_id INT NOT NULL, + post_id INT NOT NULL +); + +CREATE TABLE keyboards ( + key_number INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + name VARCHAR(255) +); + +CREATE TABLE fk_test_has_pk ( + id INT NOT NULL PRIMARY KEY +); + +CREATE TABLE fk_test_has_fk ( + id INT NOT NULL PRIMARY KEY, + fk_id INT NOT NULL, + + FOREIGN KEY (fk_id) REFERENCES fk_test_has_pk(id) +); + +--This table has an altered lock_version column name +CREATE TABLE legacy_things ( + id INT GENERATED BY DEFAULT AS IDENTITY (START WITH 10000), + tps_report_number INT DEFAULT NULL, + version INT DEFAULT 0, + PRIMARY KEY (id) +); diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/db22.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/db22.drop.sql new file mode 100644 index 00000000..df00ffd7 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/db22.drop.sql @@ -0,0 +1,2 @@ +DROP TABLE courses; + diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/db22.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/db22.sql new file mode 100644 index 00000000..853e2c73 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/db22.sql @@ -0,0 +1,5 @@ +CREATE TABLE courses ( + id INT NOT NULL PRIMARY KEY, + name VARCHAR(255) NOT NULL +); + diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/firebird.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/firebird.drop.sql new file mode 100644 index 00000000..cb454c29 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/firebird.drop.sql @@ -0,0 +1,58 @@ +DROP TABLE accounts; +DROP TABLE funny_jokes; +DROP TABLE companies; +DROP TABLE topics; +DROP TABLE developers; +DROP TABLE projects; +DROP TABLE developers_projects; +DROP TABLE orders; +DROP TABLE customers; +DROP TABLE movies; +DROP TABLE subscribers; +DROP TABLE booleantests; +DROP TABLE auto_id_tests; +DROP TABLE entrants; +DROP TABLE colnametests; +DROP TABLE mixins; +DROP TABLE people; +DROP TABLE readers; +DROP TABLE binaries; +DROP TABLE computers; +DROP TABLE posts; +DROP TABLE comments; +DROP TABLE authors; +DROP TABLE tasks; +DROP TABLE categories; +DROP TABLE categories_posts; +DROP TABLE fk_test_has_fk; +DROP TABLE fk_test_has_pk; +DROP TABLE keyboards; +DROP TABLE defaults; +DROP TABLE legacy_things; + +DROP DOMAIN D_BOOLEAN; + +DROP GENERATOR accounts_seq; +DROP GENERATOR funny_jokes_seq; +DROP GENERATOR companies_nonstd_seq; +DROP GENERATOR topics_seq; +DROP GENERATOR developers_seq; +DROP GENERATOR projects_seq; +DROP GENERATOR orders_seq; +DROP GENERATOR customers_seq; +DROP GENERATOR movies_seq; +DROP GENERATOR booleantests_seq; +DROP GENERATOR auto_id_tests_seq; +DROP GENERATOR entrants_seq; +DROP GENERATOR colnametests_seq; +DROP GENERATOR mixins_seq; +DROP GENERATOR people_seq; +DROP GENERATOR binaries_seq; +DROP GENERATOR computers_seq; +DROP GENERATOR posts_seq; +DROP GENERATOR comments_seq; +DROP GENERATOR authors_seq; +DROP GENERATOR tasks_seq; +DROP GENERATOR categories_seq; +DROP GENERATOR keyboards_seq; +DROP GENERATOR defaults_seq; diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/firebird.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/firebird.sql new file mode 100644 index 00000000..729c2097 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/firebird.sql @@ -0,0 +1,285 @@ +CREATE DOMAIN D_BOOLEAN AS SMALLINT CHECK (VALUE IN (0, 1)); + +CREATE TABLE accounts ( + id BIGINT NOT NULL, + firm_id BIGINT, + credit_limit INTEGER, + PRIMARY KEY (id) +); +CREATE GENERATOR accounts_seq; +SET GENERATOR accounts_seq TO 10000; + +CREATE TABLE funny_jokes ( + id BIGINT NOT NULL, + name VARCHAR(50), + PRIMARY KEY (id) +); +CREATE GENERATOR funny_jokes_seq; +SET GENERATOR funny_jokes_seq TO 10000; + +CREATE TABLE companies ( + id BIGINT NOT NULL, + "TYPE" VARCHAR(50), + ruby_type VARCHAR(50), + firm_id BIGINT, + name VARCHAR(50), + client_of INTEGER, + rating INTEGER DEFAULT 1, + PRIMARY KEY (id) +); +CREATE GENERATOR companies_nonstd_seq; +SET GENERATOR companies_nonstd_seq TO 10000; + +CREATE TABLE topics ( + id BIGINT NOT NULL, + title VARCHAR(255), + author_name VARCHAR(255), + author_email_address VARCHAR(255), + written_on TIMESTAMP, + bonus_time TIME, + last_read DATE, + content VARCHAR(4000), + approved D_BOOLEAN DEFAULT 1, + replies_count INTEGER DEFAULT 0, + parent_id BIGINT, + "TYPE" VARCHAR(50), + PRIMARY KEY (id) +); +CREATE GENERATOR topics_seq; +SET GENERATOR topics_seq TO 10000; + +CREATE TABLE developers ( + id BIGINT NOT NULL, + name VARCHAR(100), + salary INTEGER DEFAULT 70000, + created_at TIMESTAMP, + updated_at TIMESTAMP, + PRIMARY KEY (id) +); +CREATE GENERATOR developers_seq; +SET GENERATOR developers_seq TO 10000; + +CREATE TABLE projects ( + id BIGINT NOT NULL, + name VARCHAR(100), + "TYPE" VARCHAR(255), + PRIMARY KEY (id) +); +CREATE GENERATOR projects_seq; +SET GENERATOR projects_seq TO 10000; + +CREATE TABLE developers_projects ( + developer_id BIGINT NOT NULL, + project_id BIGINT NOT NULL, + joined_on DATE, + access_level SMALLINT DEFAULT 1 +); + +CREATE TABLE orders ( + id BIGINT NOT NULL, + name VARCHAR(100), + billing_customer_id BIGINT, + shipping_customer_id BIGINT, + PRIMARY KEY (id) +); +CREATE GENERATOR orders_seq; +SET GENERATOR orders_seq TO 10000; + +CREATE TABLE customers ( + id BIGINT NOT NULL, + name VARCHAR(100), + balance INTEGER DEFAULT 0, + address_street VARCHAR(100), + address_city VARCHAR(100), + address_country VARCHAR(100), + gps_location VARCHAR(100), + PRIMARY KEY (id) +); +CREATE GENERATOR customers_seq; +SET GENERATOR customers_seq TO 10000; + +CREATE TABLE movies ( + movieid BIGINT NOT NULL, + name varchar(100), + PRIMARY KEY (movieid) +); +CREATE GENERATOR movies_seq; +SET GENERATOR movies_seq TO 10000; + +CREATE TABLE subscribers ( + nick VARCHAR(100) NOT NULL, + name VARCHAR(100), + PRIMARY KEY (nick) +); + +CREATE TABLE booleantests ( + id BIGINT NOT NULL, + "VALUE" D_BOOLEAN, + PRIMARY KEY (id) +); +CREATE GENERATOR booleantests_seq; +SET GENERATOR booleantests_seq TO 10000; + +CREATE TABLE auto_id_tests ( + auto_id BIGINT NOT NULL, + "VALUE" INTEGER, + PRIMARY KEY (auto_id) +); +CREATE GENERATOR auto_id_tests_seq; +SET GENERATOR auto_id_tests_seq TO 10000; + +CREATE TABLE entrants ( + id BIGINT NOT NULL, + name VARCHAR(255) NOT NULL, + course_id INTEGER NOT NULL, + PRIMARY KEY (id) +); +CREATE GENERATOR entrants_seq; +SET GENERATOR entrants_seq TO 10000; + +CREATE TABLE colnametests ( + id BIGINT NOT NULL, + "REFERENCES" INTEGER NOT NULL, + PRIMARY KEY (id) +); +CREATE GENERATOR colnametests_seq; +SET GENERATOR colnametests_seq TO 10000; + +CREATE TABLE mixins ( + id BIGINT NOT NULL, + parent_id BIGINT, + pos INTEGER, + created_at TIMESTAMP, + updated_at TIMESTAMP, + lft INTEGER, + rgt INTEGER, + root_id BIGINT, + "TYPE" VARCHAR(40), + PRIMARY KEY (id) +); +CREATE GENERATOR mixins_seq; +SET GENERATOR mixins_seq TO 10000; + +CREATE TABLE people ( + id BIGINT NOT NULL, + first_name VARCHAR(40), + lock_version INTEGER DEFAULT 0 NOT NULL, + PRIMARY KEY (id) +); +CREATE GENERATOR people_seq; +SET GENERATOR people_seq TO 10000; + +CREATE TABLE readers ( + id BIGINT NOT NULL, + post_id BIGINT NOT NULL, + person_id BIGINT NOT NULL, + PRIMARY KEY (id) +); +CREATE GENERATOR readers_seq; +SET GENERATOR readers_seq TO 10000; + +CREATE TABLE binaries ( + id BIGINT NOT NULL, + data BLOB, + PRIMARY KEY (id) +); +CREATE GENERATOR binaries_seq; +SET GENERATOR binaries_seq TO 10000; + +CREATE TABLE computers ( + id BIGINT NOT NULL, + developer INTEGER NOT NULL, + "extendedWarranty" INTEGER NOT NULL, + PRIMARY KEY (id) +); +CREATE GENERATOR computers_seq; +SET GENERATOR computers_seq TO 10000; + +CREATE TABLE posts ( + id BIGINT NOT NULL, + author_id BIGINT, + title VARCHAR(255) NOT NULL, + "TYPE" VARCHAR(255) NOT NULL, + body VARCHAR(3000) NOT NULL, + PRIMARY KEY (id) +); +CREATE GENERATOR posts_seq; +SET GENERATOR posts_seq TO 10000; + +CREATE TABLE comments ( + id BIGINT NOT NULL, + post_id BIGINT NOT NULL, + "TYPE" VARCHAR(255) NOT NULL, + body VARCHAR(3000) NOT NULL, + PRIMARY KEY (id) +); +CREATE GENERATOR comments_seq; +SET GENERATOR comments_seq TO 10000; + +CREATE TABLE authors ( + id BIGINT NOT NULL, + name VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +); +CREATE GENERATOR authors_seq; +SET GENERATOR authors_seq TO 10000; + +CREATE TABLE tasks ( + id BIGINT NOT NULL, + "STARTING" TIMESTAMP, + ending TIMESTAMP, + PRIMARY KEY (id) +); +CREATE GENERATOR tasks_seq; +SET GENERATOR tasks_seq TO 10000; + +CREATE TABLE categories ( + id BIGINT NOT NULL, + name VARCHAR(255) NOT NULL, + "TYPE" VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +); +CREATE GENERATOR categories_seq; +SET GENERATOR categories_seq TO 10000; + +CREATE TABLE categories_posts ( + category_id BIGINT NOT NULL, + post_id BIGINT NOT NULL, + PRIMARY KEY (category_id, post_id) +); + +CREATE TABLE fk_test_has_pk ( + id BIGINT NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE fk_test_has_fk ( + id BIGINT NOT NULL, + fk_id BIGINT NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (fk_id) REFERENCES fk_test_has_pk(id) +); + +CREATE TABLE keyboards ( + key_number BIGINT NOT NULL, + name VARCHAR(50), + PRIMARY KEY (key_number) +); +CREATE GENERATOR keyboards_seq; +SET GENERATOR keyboards_seq TO 10000; + +CREATE TABLE defaults ( + id BIGINT NOT NULL, + default_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE GENERATOR defaults_seq; +SET GENERATOR defaults_seq TO 10000; + +CREATE TABLE legacy_things ( + id BIGINT NOT NULL, + tps_report_number INTEGER, + version INTEGER DEFAULT 0 NOT NULL, + PRIMARY KEY (id) +); +CREATE GENERATOR legacy_things_seq; +SET GENERATOR legacy_things_seq TO 10000; diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/firebird2.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/firebird2.drop.sql new file mode 100644 index 00000000..c59fb1f2 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/firebird2.drop.sql @@ -0,0 +1,2 @@ +DROP TABLE courses; +DROP GENERATOR courses_seq; diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/firebird2.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/firebird2.sql new file mode 100644 index 00000000..c1bc251f --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/firebird2.sql @@ -0,0 +1,6 @@ +CREATE TABLE courses ( + id BIGINT NOT NULL PRIMARY KEY, + name VARCHAR(255) NOT NULL +); +CREATE GENERATOR courses_seq; +SET GENERATOR courses_seq TO 10000; diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/mysql.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/mysql.drop.sql new file mode 100644 index 00000000..14df93fd --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/mysql.drop.sql @@ -0,0 +1,30 @@ +DROP TABLE accounts; +DROP TABLE funny_jokes; +DROP TABLE companies; +DROP TABLE topics; +DROP TABLE developers; +DROP TABLE projects; +DROP TABLE developers_projects; +DROP TABLE customers; +DROP TABLE orders; +DROP TABLE movies; +DROP TABLE subscribers; +DROP TABLE booleantests; +DROP TABLE auto_id_tests; +DROP TABLE entrants; +DROP TABLE colnametests; +DROP TABLE mixins; +DROP TABLE people; +DROP TABLE readers; +DROP TABLE binaries; +DROP TABLE computers; +DROP TABLE tasks; +DROP TABLE posts; +DROP TABLE comments; +DROP TABLE authors; +DROP TABLE categories; +DROP TABLE categories_posts; +DROP TABLE fk_test_has_fk; +DROP TABLE fk_test_has_pk; +DROP TABLE keyboards; +DROP TABLE legacy_things; diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/mysql.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/mysql.sql new file mode 100755 index 00000000..41071554 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/mysql.sql @@ -0,0 +1,219 @@ +CREATE TABLE `accounts` ( + `id` int(11) NOT NULL auto_increment, + `firm_id` int(11) default NULL, + `credit_limit` int(5) default NULL, + PRIMARY KEY (`id`) +) TYPE=InnoDB; + +CREATE TABLE `funny_jokes` ( + `id` int(11) NOT NULL auto_increment, + `name` varchar(50) default NULL, + PRIMARY KEY (`id`) +) TYPE=InnoDB; + +CREATE TABLE `companies` ( + `id` int(11) NOT NULL auto_increment, + `type` varchar(50) default NULL, + `ruby_type` varchar(50) default NULL, + `firm_id` int(11) default NULL, + `name` varchar(50) default NULL, + `client_of` int(11) default NULL, + `rating` int(11) default NULL default 1, + PRIMARY KEY (`id`) +) TYPE=InnoDB; + + +CREATE TABLE `topics` ( + `id` int(11) NOT NULL auto_increment, + `title` varchar(255) default NULL, + `author_name` varchar(255) default NULL, + `author_email_address` varchar(255) default NULL, + `written_on` datetime default NULL, + `bonus_time` time default NULL, + `last_read` date default NULL, + `content` text, + `approved` tinyint(1) default 1, + `replies_count` int(11) default 0, + `parent_id` int(11) default NULL, + `type` varchar(50) default NULL, + PRIMARY KEY (`id`) +) TYPE=InnoDB; + +CREATE TABLE `developers` ( + `id` int(11) NOT NULL auto_increment, + `name` varchar(100) default NULL, + `salary` int(11) default 70000, + `created_at` datetime default NULL, + `updated_at` datetime default NULL, + PRIMARY KEY (`id`) +) TYPE=InnoDB; + +CREATE TABLE `projects` ( + `id` int(11) NOT NULL auto_increment, + `name` varchar(100) default NULL, + `type` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`) +) TYPE=InnoDB; + +CREATE TABLE `developers_projects` ( + `developer_id` int(11) NOT NULL, + `project_id` int(11) NOT NULL, + `joined_on` date default NULL, + `access_level` smallint default 1 +) TYPE=InnoDB; + +CREATE TABLE `orders` ( + `id` int(11) NOT NULL auto_increment, + `name` varchar(100) default NULL, + `billing_customer_id` int(11) default NULL, + `shipping_customer_id` int(11) default NULL, + PRIMARY KEY (`id`) +) TYPE=InnoDB; + +CREATE TABLE `customers` ( + `id` int(11) NOT NULL auto_increment, + `name` varchar(100) default NULL, + `balance` int(6) default 0, + `address_street` varchar(100) default NULL, + `address_city` varchar(100) default NULL, + `address_country` varchar(100) default NULL, + `gps_location` varchar(100) default NULL, + PRIMARY KEY (`id`) +) TYPE=InnoDB; + +CREATE TABLE `movies` ( + `movieid` int(11) NOT NULL auto_increment, + `name` varchar(100) default NULL, + PRIMARY KEY (`movieid`) +) TYPE=InnoDB; + +CREATE TABLE `subscribers` ( + `nick` varchar(100) NOT NULL, + `name` varchar(100) default NULL, + PRIMARY KEY (`nick`) +) TYPE=InnoDB; + +CREATE TABLE `booleantests` ( + `id` int(11) NOT NULL auto_increment, + `value` integer default NULL, + PRIMARY KEY (`id`) +) TYPE=InnoDB; + +CREATE TABLE `auto_id_tests` ( + `auto_id` int(11) NOT NULL auto_increment, + `value` integer default NULL, + PRIMARY KEY (`auto_id`) +) TYPE=InnoDB; + +CREATE TABLE `entrants` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `course_id` INTEGER NOT NULL +); + +CREATE TABLE `colnametests` ( + `id` int(11) NOT NULL auto_increment, + `references` int(11) NOT NULL, + PRIMARY KEY (`id`) +) TYPE=InnoDB; + +CREATE TABLE `mixins` ( + `id` int(11) NOT NULL auto_increment, + `parent_id` int(11) default NULL, + `pos` int(11) default NULL, + `created_at` datetime default NULL, + `updated_at` datetime default NULL, + `lft` int(11) default NULL, + `rgt` int(11) default NULL, + `root_id` int(11) default NULL, + `type` varchar(40) default NULL, + PRIMARY KEY (`id`) +) TYPE=InnoDB; + +CREATE TABLE `people` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `first_name` VARCHAR(40) NOT NULL, + `lock_version` INTEGER NOT NULL DEFAULT 0 +) TYPE=InnoDB; + +CREATE TABLE `readers` ( + `id` int(11) NOT NULL PRIMARY KEY, + `post_id` INTEGER NOT NULL, + `person_id` INTEGER NOT NULL +) TYPE=InnoDB; + +CREATE TABLE `binaries` ( + `id` int(11) NOT NULL auto_increment, + `data` mediumblob, + PRIMARY KEY (`id`) +) TYPE=InnoDB; + +CREATE TABLE `computers` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `developer` INTEGER NOT NULL, + `extendedWarranty` INTEGER NOT NULL +) TYPE=InnoDB; + +CREATE TABLE `posts` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `author_id` INTEGER, + `title` VARCHAR(255) NOT NULL, + `body` TEXT NOT NULL, + `type` VARCHAR(255) NOT NULL +) TYPE=InnoDB; + +CREATE TABLE `comments` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `post_id` INTEGER NOT NULL, + `body` TEXT NOT NULL, + `type` VARCHAR(255) NOT NULL +) TYPE=InnoDB; + +CREATE TABLE `authors` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `name` VARCHAR(255) NOT NULL +) TYPE=InnoDB; + +CREATE TABLE `tasks` ( + `id` int(11) NOT NULL auto_increment, + `starting` datetime NOT NULL default '0000-00-00 00:00:00', + `ending` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`id`) +) TYPE=InnoDB; + +CREATE TABLE `categories` ( + `id` int(11) NOT NULL auto_increment, + `name` VARCHAR(255) NOT NULL, + `type` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`) +) TYPE=InnoDB; + +CREATE TABLE `categories_posts` ( + `category_id` int(11) NOT NULL, + `post_id` int(11) NOT NULL +) TYPE=InnoDB; + +CREATE TABLE `fk_test_has_pk` ( + `id` INTEGER NOT NULL PRIMARY KEY +) TYPE=InnoDB; + +CREATE TABLE `fk_test_has_fk` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `fk_id` INTEGER NOT NULL, + + FOREIGN KEY (`fk_id`) REFERENCES `fk_test_has_pk`(`id`) +) TYPE=InnoDB; + + +CREATE TABLE `keyboards` ( + `key_number` int(11) NOT NULL auto_increment primary key, + `name` varchar(50) default NULL +); + +-- Altered lock_version column name. +CREATE TABLE `legacy_things` ( + `id` int(11) NOT NULL auto_increment, + `tps_report_number` int(11) default NULL, + `version` int(11) NOT NULL default 0, + PRIMARY KEY (`id`) +) TYPE=InnoDB; diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/mysql2.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/mysql2.drop.sql new file mode 100644 index 00000000..df00ffd7 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/mysql2.drop.sql @@ -0,0 +1,2 @@ +DROP TABLE courses; + diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/mysql2.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/mysql2.sql new file mode 100644 index 00000000..0bfd2e6f --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/mysql2.sql @@ -0,0 +1,5 @@ +CREATE TABLE `courses` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `name` VARCHAR(255) NOT NULL +) TYPE=InnoDB; + diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/openbase.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/openbase.drop.sql new file mode 100644 index 00000000..fb40e3f2 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/openbase.drop.sql @@ -0,0 +1,2 @@ +DROP ALL +go \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/openbase.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/openbase.sql new file mode 100644 index 00000000..9ca1a7d0 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/openbase.sql @@ -0,0 +1,282 @@ +CREATE TABLE accounts ( + id integer UNIQUE INDEX DEFAULT _rowid, + firm_id integer, + credit_limit integer +) +go +CREATE PRIMARY KEY accounts (id) +go + +CREATE TABLE funny_jokes ( + id integer UNIQUE INDEX DEFAULT _rowid, + name char(50) DEFAULT NULL +) +go +CREATE PRIMARY KEY funny_jokes (id) +go + +CREATE TABLE companies ( + id integer UNIQUE INDEX DEFAULT _rowid, + type char(50), + ruby_type char(50), + firm_id integer, + name char(50), + client_of integer, + rating integer default 1 +) +go +CREATE PRIMARY KEY companies (id) +go + +CREATE TABLE developers_projects ( + developer_id integer NOT NULL, + project_id integer NOT NULL, + joined_on date, + access_level integer default 1 +) +go + +CREATE TABLE developers ( + id integer UNIQUE INDEX DEFAULT _rowid, + name char(100), + salary integer DEFAULT 70000, + created_at datetime, + updated_at datetime +) +go +CREATE PRIMARY KEY developers (id) +go + +CREATE TABLE projects ( + id integer UNIQUE INDEX DEFAULT _rowid, + name char(100), + type char(255) +) +go +CREATE PRIMARY KEY projects (id) +go + +CREATE TABLE topics ( + id integer UNIQUE INDEX DEFAULT _rowid, + title char(255), + author_name char(255), + author_email_address char(255), + written_on datetime, + bonus_time time, + last_read date, + content char(4096), + approved boolean default true, + replies_count integer default 0, + parent_id integer, + type char(50) +) +go +CREATE PRIMARY KEY topics (id) +go + +CREATE TABLE customers ( + id integer UNIQUE INDEX DEFAULT _rowid, + name char, + balance integer default 0, + address_street char, + address_city char, + address_country char, + gps_location char +) +go +CREATE PRIMARY KEY customers (id) +go + +CREATE TABLE orders ( + id integer UNIQUE INDEX DEFAULT _rowid, + name char, + billing_customer_id integer, + shipping_customer_id integer +) +go +CREATE PRIMARY KEY orders (id) +go + +CREATE TABLE movies ( + movieid integer UNIQUE INDEX DEFAULT _rowid, + name text +) +go +CREATE PRIMARY KEY movies (movieid) +go + +CREATE TABLE subscribers ( + nick CHAR(100) NOT NULL DEFAULT _rowid, + name CHAR(100) +) +go +CREATE PRIMARY KEY subscribers (nick) +go + +CREATE TABLE booleantests ( + id integer UNIQUE INDEX DEFAULT _rowid, + value boolean +) +go +CREATE PRIMARY KEY booleantests (id) +go + +CREATE TABLE defaults ( + id integer UNIQUE INDEX , + modified_date date default CURDATE(), + modified_date_function date default NOW(), + fixed_date date default '2004-01-01', + modified_time timestamp default NOW(), + modified_time_function timestamp default NOW(), + fixed_time timestamp default '2004-01-01 00:00:00.000000-00', + char1 char(1) default 'Y', + char2 char(50) default 'a char field', + char3 text default 'a text field' +) +go + +CREATE TABLE auto_id_tests ( + auto_id integer UNIQUE INDEX DEFAULT _rowid, + value integer +) +go +CREATE PRIMARY KEY auto_id_tests (auto_id) +go + +CREATE TABLE entrants ( + id integer UNIQUE INDEX , + name text, + course_id integer +) +go + +CREATE TABLE colnametests ( + id integer UNIQUE INDEX , + references integer NOT NULL +) +go + +CREATE TABLE mixins ( + id integer UNIQUE INDEX DEFAULT _rowid, + parent_id integer, + type char, + pos integer, + lft integer, + rgt integer, + root_id integer, + created_at timestamp, + updated_at timestamp +) +go +CREATE PRIMARY KEY mixins (id) +go + +CREATE TABLE people ( + id integer UNIQUE INDEX DEFAULT _rowid, + first_name text, + lock_version integer default 0 +) +go +CREATE PRIMARY KEY people (id) +go + +CREATE TABLE readers ( + id integer UNIQUE INDEX DEFAULT _rowid, + post_id integer NOT NULL, + person_id integer NOT NULL +) +go +CREATE PRIMARY KEY readers (id) +go + +CREATE TABLE binaries ( + id integer UNIQUE INDEX DEFAULT _rowid, + data object +) +go +CREATE PRIMARY KEY binaries (id) +go + +CREATE TABLE computers ( + id integer UNIQUE INDEX , + developer integer NOT NULL, + extendedWarranty integer NOT NULL +) +go + +CREATE TABLE posts ( + id integer UNIQUE INDEX , + author_id integer, + title char(255), + type char(255), + body text +) +go + +CREATE TABLE comments ( + id integer UNIQUE INDEX , + post_id integer, + type char(255), + body text +) +go + +CREATE TABLE authors ( + id integer UNIQUE INDEX , + name char(255) default NULL +) +go + +CREATE TABLE tasks ( + id integer UNIQUE INDEX DEFAULT _rowid, + starting datetime, + ending datetime +) +go +CREATE PRIMARY KEY tasks (id) +go + +CREATE TABLE categories ( + id integer UNIQUE INDEX , + name char(255), + type char(255) +) +go + +CREATE TABLE categories_posts ( + category_id integer NOT NULL, + post_id integer NOT NULL +) +go + +CREATE TABLE fk_test_has_pk ( + id INTEGER NOT NULL DEFAULT _rowid +) +go +CREATE PRIMARY KEY fk_test_has_pk (id) +go + +CREATE TABLE fk_test_has_fk ( + id INTEGER NOT NULL DEFAULT _rowid, + fk_id INTEGER NOT NULL REFERENCES fk_test_has_pk.id +) +go +CREATE PRIMARY KEY fk_test_has_fk (id) +go + +CREATE TABLE keyboards ( + key_number integer UNIQUE INDEX DEFAULT _rowid, + name char(50) +) +go +CREATE PRIMARY KEY keyboards (key_number) +go + +CREATE TABLE legacy_things ( + id INTEGER NOT NULL DEFAULT _rowid, + tps_report_number INTEGER default NULL, + version integer NOT NULL default 0 +) +go +CREATE PRIMARY KEY legacy_things (id) +go \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/openbase2.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/openbase2.drop.sql new file mode 100644 index 00000000..ea1571da --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/openbase2.drop.sql @@ -0,0 +1,2 @@ +DROP TABLE courses +go diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/openbase2.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/openbase2.sql new file mode 100644 index 00000000..a37c4f4c --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/openbase2.sql @@ -0,0 +1,7 @@ +CREATE TABLE courses ( + id integer UNIQUE INDEX DEFAULT _rowid, + name text +) +go +CREATE PRIMARY KEY courses (id) +go \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/oracle.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/oracle.drop.sql new file mode 100644 index 00000000..4d4ddb83 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/oracle.drop.sql @@ -0,0 +1,61 @@ +drop table accounts; +drop table funny_jokes; +drop table companies; +drop table topics; +drop synonym subjects; +drop table developers_projects; +drop table computers; +drop table developers; +drop table projects; +drop table customers; +drop table orders; +drop table movies; +drop table subscribers; +drop table booleantests; +drop table auto_id_tests; +drop table entrants; +drop table colnametests; +drop table mixins; +drop table people; +drop table readers; +drop table binaries; +drop table comments; +drop table authors; +drop table tasks; +drop table categories_posts; +drop table categories; +drop table posts; +drop table fk_test_has_pk; +drop table fk_test_has_fk; +drop table keyboards; +drop table legacy_things; + +drop sequence accounts_seq; +drop sequence funny_jokes_seq; +drop sequence companies_nonstd_seq; +drop sequence topics_seq; +drop sequence developers_seq; +drop sequence projects_seq; +drop sequence developers_projects_seq; +drop sequence customers_seq; +drop sequence orders_seq; +drop sequence movies_seq; +drop sequence subscribers_seq; +drop sequence booleantests_seq; +drop sequence auto_id_tests_seq; +drop sequence entrants_seq; +drop sequence colnametests_seq; +drop sequence mixins_seq; +drop sequence people_seq; +drop sequence binaries_seq; +drop sequence posts_seq; +drop sequence comments_seq; +drop sequence authors_seq; +drop sequence tasks_seq; +drop sequence computers_seq; +drop sequence categories_seq; +drop sequence categories_posts_seq; +drop sequence fk_test_has_pk_seq; +drop sequence fk_test_has_fk_seq; +drop sequence keyboards_seq; +drop sequence legacy_things_seq; diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/oracle.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/oracle.sql new file mode 100644 index 00000000..39099b17 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/oracle.sql @@ -0,0 +1,292 @@ +create table companies ( + id integer not null, + type varchar(50) default null, + ruby_type varchar(50) default null, + firm_id integer default null references companies initially deferred disable, + name varchar(50) default null, + client_of integer default null references companies initially deferred disable, + companies_count integer default 0, + rating integer default 1, + primary key (id) +); + +-- non-standard sequence name used to test set_sequence_name +-- +create sequence companies_nonstd_seq minvalue 10000; + +create table funny_jokes ( + id integer not null, + name varchar(50) default null, + primary key (id) +); +create sequence funny_jokes_seq minvalue 10000; + +create table accounts ( + id integer not null, + firm_id integer default null references companies initially deferred disable, + credit_limit integer default null +); +create sequence accounts_seq minvalue 10000; + +create table topics ( + id integer not null, + title varchar(255) default null, + author_name varchar(255) default null, + author_email_address varchar(255) default null, + written_on timestamp default null, + bonus_time timestamp default null, + last_read timestamp default null, + content varchar(4000), + approved integer default 1, + replies_count integer default 0, + parent_id integer references topics initially deferred disable, + type varchar(50) default null, + primary key (id) +); +-- try again for 8i +create table topics ( + id integer not null, + title varchar(255) default null, + author_name varchar(255) default null, + author_email_address varchar(255) default null, + written_on date default null, + bonus_time date default null, + last_read date default null, + content varchar(4000), + approved integer default 1, + replies_count integer default 0, + parent_id integer references topics initially deferred disable, + type varchar(50) default null, + primary key (id) +); +create sequence topics_seq minvalue 10000; + +create synonym subjects for topics; + +create table developers ( + id integer not null, + name varchar(100) default null, + salary integer default 70000, + created_at timestamp default null, + updated_at timestamp default null, + primary key (id) +); +create sequence developers_seq minvalue 10000; + +create table projects ( + id integer not null, + name varchar(100) default null, + type varchar(255) default null, + primary key (id) +); +create sequence projects_seq minvalue 10000; + +create table developers_projects ( + developer_id integer not null references developers initially deferred disable, + project_id integer not null references projects initially deferred disable, + joined_on timestamp default null, + access_level integer default 1 +); +-- Try again for 8i +create table developers_projects ( + developer_id integer not null references developers initially deferred disable, + project_id integer not null references projects initially deferred disable, + joined_on date default null +); +create sequence developers_projects_seq minvalue 10000; + +create table orders ( + id integer not null, + name varchar(100) default null, + billing_customer_id integer default null, + shipping_customer_id integer default null, + primary key (id) +); +create sequence orders_seq minvalue 10000; + +create table customers ( + id integer not null, + name varchar(100) default null, + balance integer default 0, + address_street varchar(100) default null, + address_city varchar(100) default null, + address_country varchar(100) default null, + gps_location varchar(100) default null, + primary key (id) +); +create sequence customers_seq minvalue 10000; + +create table movies ( + movieid integer not null, + name varchar(100) default null, + primary key (movieid) +); +create sequence movies_seq minvalue 10000; + +create table subscribers ( + nick varchar(100) not null, + name varchar(100) default null, + primary key (nick) +); +create sequence subscribers_seq minvalue 10000; + +create table booleantests ( + id integer not null, + value integer default null, + primary key (id) +); +create sequence booleantests_seq minvalue 10000; + +create table auto_id_tests ( + auto_id integer not null, + value integer default null, + primary key (auto_id) +); +create sequence auto_id_tests_seq minvalue 10000; + +create table entrants ( + id integer not null primary key, + name varchar(255) not null, + course_id integer not null +); +create sequence entrants_seq minvalue 10000; + +create table colnametests ( + id integer not null, + references integer not null, + primary key (id) +); +create sequence colnametests_seq minvalue 10000; + +create table mixins ( + id integer not null, + parent_id integer default null references mixins initially deferred disable, + type varchar(40) default null, + pos integer default null, + lft integer default null, + rgt integer default null, + root_id integer default null, + created_at timestamp default null, + updated_at timestamp default null, + primary key (id) +); +-- try again for 8i +create table mixins ( + id integer not null, + parent_id integer default null references mixins initially deferred disable, + type varchar(40) default null, + pos integer default null, + lft integer default null, + rgt integer default null, + root_id integer default null, + created_at date default null, + updated_at date default null, + primary key (id) +); +create sequence mixins_seq minvalue 10000; + +create table people ( + id integer not null, + first_name varchar(40) null, + lock_version integer default 0, + primary key (id) +); +create sequence people_seq minvalue 10000; + +create table readers ( + id integer not null, + post_id integer not null, + person_id integer not null, + primary key (id) +); +create sequence readers_seq minvalue 10000; + +create table binaries ( + id integer not null, + data blob null, + primary key (id) +); +create sequence binaries_seq minvalue 10000; + +create table computers ( + id integer not null primary key, + developer integer not null references developers initially deferred disable, + "extendedWarranty" integer not null +); +create sequence computers_seq minvalue 10000; + +create table posts ( + id integer not null primary key, + author_id integer default null, + title varchar(255) default null, + type varchar(255) default null, + body varchar(3000) default null +); +create sequence posts_seq minvalue 10000; + +create table comments ( + id integer not null primary key, + post_id integer default null, + type varchar(255) default null, + body varchar(3000) default null +); +create sequence comments_seq minvalue 10000; + +create table authors ( + id integer not null primary key, + name varchar(255) default null +); +create sequence authors_seq minvalue 10000; + +create table tasks ( + id integer not null primary key, + starting date default null, + ending date default null +); +create sequence tasks_seq minvalue 10000; + +create table categories ( + id integer not null primary key, + name varchar(255) default null, + type varchar(255) default null +); +create sequence categories_seq minvalue 10000; + +create table categories_posts ( + category_id integer not null references categories initially deferred disable, + post_id integer not null references posts initially deferred disable +); +create sequence categories_posts_seq minvalue 10000; + +create table fk_test_has_pk ( + id integer not null primary key +); +create sequence fk_test_has_pk_seq minvalue 10000; + +create table fk_test_has_fk ( + id integer not null primary key, + fk_id integer not null references fk_test_has_fk initially deferred disable +); +create sequence fk_test_has_fk_seq minvalue 10000; + +create table keyboards ( + key_number integer not null, + name varchar(50) default null +); +create sequence keyboards_seq minvalue 10000; + +create table test_oracle_defaults ( + id integer not null primary key, + test_char char(1) default 'X' not null, + test_string varchar2(20) default 'hello' not null, + test_int integer default 3 not null +); +create sequence test_oracle_defaults_seq minvalue 10000; + +--This table has an altered lock_version column name. +create table legacy_things ( + id integer not null primary key, + tps_report_number integer default null, + version integer default 0 +); +create sequence legacy_things_seq minvalue 10000; diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/oracle2.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/oracle2.drop.sql new file mode 100644 index 00000000..abe7e55c --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/oracle2.drop.sql @@ -0,0 +1,2 @@ +drop table courses; +drop sequence courses_seq; diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/oracle2.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/oracle2.sql new file mode 100644 index 00000000..3c171f4f --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/oracle2.sql @@ -0,0 +1,6 @@ +create table courses ( + id int not null primary key, + name varchar(255) not null +); + +create sequence courses_seq minvalue 10000; diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/postgresql.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/postgresql.drop.sql new file mode 100644 index 00000000..26628392 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/postgresql.drop.sql @@ -0,0 +1,34 @@ +DROP SEQUENCE accounts_id_seq; +DROP TABLE accounts; +DROP TABLE funny_jokes; +DROP TABLE companies; +DROP SEQUENCE companies_nonstd_seq; +DROP TABLE topics; +DROP TABLE developers; +DROP TABLE projects; +DROP TABLE developers_projects; +DROP TABLE customers; +DROP TABLE orders; +DROP TABLE movies; +DROP TABLE subscribers; +DROP TABLE booleantests; +DROP TABLE auto_id_tests; +DROP TABLE entrants; +DROP TABLE colnametests; +DROP TABLE mixins; +DROP TABLE people; +DROP TABLE readers; +DROP TABLE binaries; +DROP TABLE computers; +DROP TABLE posts; +DROP TABLE comments; +DROP TABLE authors; +DROP TABLE tasks; +DROP TABLE categories; +DROP TABLE categories_posts; +DROP TABLE defaults; +DROP TABLE fk_test_has_fk; +DROP TABLE fk_test_has_pk; +DROP TABLE geometrics; +DROP TABLE keyboards; +DROP TABLE legacy_things; diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/postgresql.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/postgresql.sql new file mode 100644 index 00000000..175e8494 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/postgresql.sql @@ -0,0 +1,248 @@ +CREATE SEQUENCE public.accounts_id_seq START 100; + +CREATE TABLE accounts ( + id integer DEFAULT nextval('public.accounts_id_seq'), + firm_id integer, + credit_limit integer, + PRIMARY KEY (id) +); + +CREATE TABLE funny_jokes ( + id serial, + name character varying(50) +); + +CREATE SEQUENCE companies_nonstd_seq START 101; + +CREATE TABLE companies ( + id integer DEFAULT nextval('companies_nonstd_seq'), + "type" character varying(50), + "ruby_type" character varying(50), + firm_id integer, + name character varying(50), + client_of integer, + rating integer default 1, + PRIMARY KEY (id) +); + +CREATE TABLE developers_projects ( + developer_id integer NOT NULL, + project_id integer NOT NULL, + joined_on date, + access_level integer default 1 +); + +CREATE TABLE developers ( + id serial, + name character varying(100), + salary integer DEFAULT 70000, + created_at timestamp, + updated_at timestamp, + PRIMARY KEY (id) +); +SELECT setval('developers_id_seq', 100); + +CREATE TABLE projects ( + id serial, + name character varying(100), + type varchar(255), + PRIMARY KEY (id) +); +SELECT setval('projects_id_seq', 100); + +CREATE TABLE topics ( + id serial, + title character varying(255), + author_name character varying(255), + author_email_address character varying(255), + written_on timestamp without time zone, + bonus_time time, + last_read date, + content text, + approved boolean default true, + replies_count integer default 0, + parent_id integer, + "type" character varying(50), + PRIMARY KEY (id) +); +SELECT setval('topics_id_seq', 100); + +CREATE TABLE customers ( + id serial, + name character varying, + balance integer default 0, + address_street character varying, + address_city character varying, + address_country character varying, + gps_location character varying, + PRIMARY KEY (id) +); +SELECT setval('customers_id_seq', 100); + +CREATE TABLE orders ( + id serial, + name character varying, + billing_customer_id integer, + shipping_customer_id integer, + PRIMARY KEY (id) +); +SELECT setval('orders_id_seq', 100); + +CREATE TABLE movies ( + movieid serial, + name text, + PRIMARY KEY (movieid) +); + +CREATE TABLE subscribers ( + nick text NOT NULL, + name text, + PRIMARY KEY (nick) +); + +CREATE TABLE booleantests ( + id serial, + value boolean, + PRIMARY KEY (id) +); + +CREATE TABLE defaults ( + id serial, + modified_date date default CURRENT_DATE, + modified_date_function date default now(), + fixed_date date default '2004-01-01', + modified_time timestamp default CURRENT_TIMESTAMP, + modified_time_function timestamp default now(), + fixed_time timestamp default '2004-01-01 00:00:00.000000-00', + char1 char(1) default 'Y', + char2 character varying(50) default 'a varchar field', + char3 text default 'a text field', + positive_integer integer default 1, + negative_integer integer default -1 +); + +CREATE TABLE auto_id_tests ( + auto_id serial, + value integer, + PRIMARY KEY (auto_id) +); + +CREATE TABLE entrants ( + id serial, + name text, + course_id integer +); + +CREATE TABLE colnametests ( + id serial, + "references" integer NOT NULL +); + +CREATE TABLE mixins ( + id serial, + parent_id integer, + type character varying, + pos integer, + lft integer, + rgt integer, + root_id integer, + created_at timestamp, + updated_at timestamp, + PRIMARY KEY (id) +); + +CREATE TABLE people ( + id serial, + first_name text, + lock_version integer default 0, + PRIMARY KEY (id) +); + +CREATE TABLE readers ( + id serial, + post_id integer NOT NULL, + person_id integer NOT NULL, + primary key (id) +); + +CREATE TABLE binaries ( + id serial , + data bytea, + PRIMARY KEY (id) +); + +CREATE TABLE computers ( + id serial, + developer integer NOT NULL, + "extendedWarranty" integer NOT NULL +); + +CREATE TABLE posts ( + id serial, + author_id integer, + title varchar(255), + type varchar(255), + body text +); + +CREATE TABLE comments ( + id serial, + post_id integer, + type varchar(255), + body text +); + +CREATE TABLE authors ( + id serial, + name varchar(255) default NULL +); + +CREATE TABLE tasks ( + id serial, + starting timestamp, + ending timestamp, + PRIMARY KEY (id) +); + +CREATE TABLE categories ( + id serial, + name varchar(255), + type varchar(255) +); + +CREATE TABLE categories_posts ( + category_id integer NOT NULL, + post_id integer NOT NULL +); + +CREATE TABLE fk_test_has_pk ( + id INTEGER NOT NULL PRIMARY KEY +); + +CREATE TABLE fk_test_has_fk ( + id INTEGER NOT NULL PRIMARY KEY, + fk_id INTEGER NOT NULL REFERENCES fk_test_has_fk(id) +); + +CREATE TABLE geometrics ( + id serial primary key, + a_point point, + -- a_line line, (the line type is currently not implemented in postgresql) + a_line_segment lseg, + a_box box, + a_path path, + a_polygon polygon, + a_circle circle +); + +CREATE TABLE keyboards ( + key_number serial primary key, + "name" character varying(50) +); + +--Altered lock_version column name. +CREATE TABLE legacy_things ( + id serial primary key, + tps_report_number integer, + version integer default 0 +); diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/postgresql2.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/postgresql2.drop.sql new file mode 100644 index 00000000..df00ffd7 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/postgresql2.drop.sql @@ -0,0 +1,2 @@ +DROP TABLE courses; + diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/postgresql2.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/postgresql2.sql new file mode 100644 index 00000000..c0d7f79b --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/postgresql2.sql @@ -0,0 +1,5 @@ +CREATE TABLE courses ( + id serial, + name text +); + diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/schema.rb b/vendor/rails/activerecord/test/fixtures/db_definitions/schema.rb new file mode 100644 index 00000000..7d10fbca --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/schema.rb @@ -0,0 +1,32 @@ +ActiveRecord::Schema.define do + + create_table :taggings, :force => true do |t| + t.column :tag_id, :integer + t.column :super_tag_id, :integer + t.column :taggable_type, :string + t.column :taggable_id, :integer + end + + create_table :tags, :force => true do |t| + t.column :name, :string + t.column :taggings_count, :integer, :default => 0 + end + + create_table :categorizations, :force => true do |t| + t.column :category_id, :integer + t.column :post_id, :integer + t.column :author_id, :integer + end + + add_column :posts, :taggings_count, :integer, :default => 0 + add_column :authors, :author_address_id, :integer + + create_table :author_addresses, :force => true do |t| + t.column :author_address_id, :integer + end + + create_table :author_favorites, :force => true do |t| + t.column :author_id, :integer + t.column :favorite_author_id, :integer + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/sqlite.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlite.drop.sql new file mode 100644 index 00000000..14df93fd --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlite.drop.sql @@ -0,0 +1,30 @@ +DROP TABLE accounts; +DROP TABLE funny_jokes; +DROP TABLE companies; +DROP TABLE topics; +DROP TABLE developers; +DROP TABLE projects; +DROP TABLE developers_projects; +DROP TABLE customers; +DROP TABLE orders; +DROP TABLE movies; +DROP TABLE subscribers; +DROP TABLE booleantests; +DROP TABLE auto_id_tests; +DROP TABLE entrants; +DROP TABLE colnametests; +DROP TABLE mixins; +DROP TABLE people; +DROP TABLE readers; +DROP TABLE binaries; +DROP TABLE computers; +DROP TABLE tasks; +DROP TABLE posts; +DROP TABLE comments; +DROP TABLE authors; +DROP TABLE categories; +DROP TABLE categories_posts; +DROP TABLE fk_test_has_fk; +DROP TABLE fk_test_has_pk; +DROP TABLE keyboards; +DROP TABLE legacy_things; diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/sqlite.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlite.sql new file mode 100644 index 00000000..5a7fec3d --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlite.sql @@ -0,0 +1,201 @@ +CREATE TABLE 'accounts' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'firm_id' INTEGER DEFAULT NULL, + 'credit_limit' INTEGER DEFAULT NULL +); + +CREATE TABLE 'funny_jokes' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL +); + +CREATE TABLE 'companies' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'type' VARCHAR(255) DEFAULT NULL, + 'ruby_type' VARCHAR(255) DEFAULT NULL, + 'firm_id' INTEGER DEFAULT NULL, + 'name' TEXT DEFAULT NULL, + 'client_of' INTEGER DEFAULT NULL, + 'rating' INTEGER DEFAULT 1 +); + + +CREATE TABLE 'topics' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'title' VARCHAR(255) DEFAULT NULL, + 'author_name' VARCHAR(255) DEFAULT NULL, + 'author_email_address' VARCHAR(255) DEFAULT NULL, + 'written_on' DATETIME DEFAULT NULL, + 'bonus_time' TIME DEFAULT NULL, + 'last_read' DATE DEFAULT NULL, + 'content' TEXT, + 'approved' boolean DEFAULT 't', + 'replies_count' INTEGER DEFAULT 0, + 'parent_id' INTEGER DEFAULT NULL, + 'type' VARCHAR(255) DEFAULT NULL +); + +CREATE TABLE 'developers' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL, + 'salary' INTEGER DEFAULT 70000, + 'created_at' DATETIME DEFAULT NULL, + 'updated_at' DATETIME DEFAULT NULL +); + +CREATE TABLE 'projects' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL, + 'type' VARCHAR(255) DEFAULT NULL +); + +CREATE TABLE 'developers_projects' ( + 'developer_id' INTEGER NOT NULL, + 'project_id' INTEGER NOT NULL, + 'joined_on' DATE DEFAULT NULL, + 'access_level' INTEGER DEFAULT 1 +); + + +CREATE TABLE 'orders' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' VARCHAR(255) DEFAULT NULL, + 'billing_customer_id' INTEGER DEFAULT NULL, + 'shipping_customer_id' INTEGER DEFAULT NULL +); + +CREATE TABLE 'customers' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' VARCHAR(255) DEFAULT NULL, + 'balance' INTEGER DEFAULT 0, + 'address_street' TEXT DEFAULT NULL, + 'address_city' TEXT DEFAULT NULL, + 'address_country' TEXT DEFAULT NULL, + 'gps_location' TEXT DEFAULT NULL +); + +CREATE TABLE 'movies' ( + 'movieid' INTEGER PRIMARY KEY NOT NULL, + 'name' VARCHAR(255) DEFAULT NULL +); + +CREATE TABLE subscribers ( + 'nick' VARCHAR(255) PRIMARY KEY NOT NULL, + 'name' VARCHAR(255) DEFAULT NULL +); + +CREATE TABLE 'booleantests' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'value' INTEGER DEFAULT NULL +); + +CREATE TABLE 'auto_id_tests' ( + 'auto_id' INTEGER PRIMARY KEY NOT NULL, + 'value' INTEGER DEFAULT NULL +); + +CREATE TABLE 'entrants' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'name' VARCHAR(255) NOT NULL, + 'course_id' INTEGER NOT NULL +); + +CREATE TABLE 'colnametests' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'references' INTEGER NOT NULL +); + +CREATE TABLE 'mixins' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'parent_id' INTEGER DEFAULT NULL, + 'type' VARCHAR(40) DEFAULT NULL, + 'pos' INTEGER DEFAULT NULL, + 'lft' INTEGER DEFAULT NULL, + 'rgt' INTEGER DEFAULT NULL, + 'root_id' INTEGER DEFAULT NULL, + 'created_at' DATETIME DEFAULT NULL, + 'updated_at' DATETIME DEFAULT NULL +); + +CREATE TABLE 'people' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'first_name' VARCHAR(40) DEFAULT NULL, + 'lock_version' INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE 'readers' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'post_id' INTEGER NOT NULL, + 'person_id' INTEGER NOT NULL +); + +CREATE TABLE 'binaries' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'data' BLOB DEFAULT NULL +); + +CREATE TABLE 'computers' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'developer' INTEGER NOT NULL, + 'extendedWarranty' INTEGER NOT NULL +); + +CREATE TABLE 'posts' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'author_id' INTEGER, + 'title' VARCHAR(255) NOT NULL, + 'type' VARCHAR(255) NOT NULL, + 'body' TEXT NOT NULL +); + +CREATE TABLE 'comments' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'post_id' INTEGER NOT NULL, + 'type' VARCHAR(255) NOT NULL, + 'body' TEXT NOT NULL +); + +CREATE TABLE 'authors' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'name' VARCHAR(255) NOT NULL +); + +CREATE TABLE 'tasks' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'starting' DATETIME DEFAULT NULL, + 'ending' DATETIME DEFAULT NULL +); + +CREATE TABLE 'categories' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'name' VARCHAR(255) NOT NULL, + 'type' VARCHAR(255) DEFAULT NULL +); + +CREATE TABLE 'categories_posts' ( + 'category_id' INTEGER NOT NULL, + 'post_id' INTEGER NOT NULL +); + +CREATE TABLE 'fk_test_has_pk' ( + 'id' INTEGER NOT NULL PRIMARY KEY +); + +CREATE TABLE 'fk_test_has_fk' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'fk_id' INTEGER NOT NULL, + + FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('id') +); + +CREATE TABLE 'keyboards' ( + 'key_number' INTEGER PRIMARY KEY NOT NULL, + 'name' VARCHAR(255) DEFAULT NULL +); + +--Altered lock_version column name. +CREATE TABLE 'legacy_things' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'tps_report_number' INTEGER DEFAULT NULL, + 'version' INTEGER NOT NULL DEFAULT 0 +) diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/sqlite2.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlite2.drop.sql new file mode 100644 index 00000000..df00ffd7 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlite2.drop.sql @@ -0,0 +1,2 @@ +DROP TABLE courses; + diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/sqlite2.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlite2.sql new file mode 100644 index 00000000..5c0d231b --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlite2.sql @@ -0,0 +1,5 @@ +CREATE TABLE 'courses' ( + 'id' INTEGER NOT NULL PRIMARY KEY, + 'name' VARCHAR(255) NOT NULL +); + diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver.drop.sql new file mode 100644 index 00000000..ea14697b --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver.drop.sql @@ -0,0 +1,30 @@ +DROP TABLE accounts; +DROP TABLE funny_jokes; +DROP TABLE companies; +DROP TABLE topics; +DROP TABLE developers; +DROP TABLE projects; +DROP TABLE developers_projects; +DROP TABLE customers; +DROP TABLE orders; +DROP TABLE movies; +DROP TABLE subscribers; +DROP TABLE booleantests; +DROP TABLE auto_id_tests; +DROP TABLE entrants; +DROP TABLE colnametests; +DROP TABLE mixins; +DROP TABLE people; +DROP TABLE readers; +DROP TABLE binaries; +DROP TABLE computers; +DROP TABLE posts; +DROP TABLE comments; +DROP TABLE authors; +DROP TABLE tasks; +DROP TABLE categories; +DROP TABLE categories_posts; +DROP TABLE fk_test_has_fk; +DROP TABLE fk_test_has_pk; +DROP TABLE keyboards; +DROP TABLE legacy_things; diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver.sql new file mode 100644 index 00000000..acbcaa13 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver.sql @@ -0,0 +1,203 @@ +CREATE TABLE accounts ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + firm_id int default NULL, + credit_limit int default NULL +); + +CREATE TABLE funny_jokes ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + name varchar(50) default NULL +); + +CREATE TABLE companies ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + type varchar(50) default NULL, + ruby_type varchar(50) default NULL, + firm_id int default NULL, + name varchar(50) default NULL, + client_of int default NULL, + rating int default 1 +); + +CREATE TABLE topics ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + title varchar(255) default NULL, + author_name varchar(255) default NULL, + author_email_address varchar(255) default NULL, + written_on datetime default NULL, + bonus_time datetime default NULL, + last_read datetime default NULL, + content varchar(255) default NULL, + approved bit default 1, + replies_count int default 0, + parent_id int default NULL, + type varchar(50) default NULL +); + +CREATE TABLE developers ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + name varchar(100) default NULL, + salary int default 70000, + created_at datetime default NULL, + updated_at datetime default NULL +); + +CREATE TABLE projects ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + name varchar(100) default NULL, + type varchar(255) default NULL +); + +CREATE TABLE developers_projects ( + developer_id int NOT NULL, + project_id int NOT NULL, + joined_on datetime default NULL, + access_level int default 1 +); + +CREATE TABLE orders ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + name varchar(100) default NULL, + billing_customer_id int default NULL, + shipping_customer_id int default NULL +); + + +CREATE TABLE customers ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + name varchar(100) default NULL, + balance int default 0, + address_street varchar(100) default NULL, + address_city varchar(100) default NULL, + address_country varchar(100) default NULL, + gps_location varchar(100) default NULL +); + +CREATE TABLE movies ( + movieid int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + name varchar(100) default NULL +); + +CREATE TABLE subscribers ( + nick varchar(100) NOT NULL PRIMARY KEY, + name varchar(100) default NULL +); + +CREATE TABLE booleantests ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + value bit default NULL +); + +CREATE TABLE auto_id_tests ( + auto_id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + value int default NULL +); + +CREATE TABLE entrants ( + id int NOT NULL PRIMARY KEY, + name varchar(255) NOT NULL, + course_id int NOT NULL +); + +CREATE TABLE colnametests ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + [references] int NOT NULL +); + +CREATE TABLE mixins ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + parent_id int default NULL, + pos int default NULL, + created_at datetime default NULL, + updated_at datetime default NULL, + lft int default NULL, + rgt int default NULL, + root_id int default NULL, + type varchar(40) default NULL +); + +CREATE TABLE people ( + id int NOT NULL IDENTITY(1, 1), + first_name varchar(40) NULL, + lock_version int default 0, + PRIMARY KEY (id) +); + +CREATE TABLE readers ( + id int NOT NULL IDENTITY(1, 1), + post_id int NOT NULL, + person_id int NOT NULL, + primary key (id) +); + +CREATE TABLE binaries ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + data image NULL +); + +CREATE TABLE computers ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + developer int NOT NULL, + extendedWarranty int NOT NULL +); + +CREATE TABLE posts ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + author_id int default NULL, + title varchar(255) default NULL, + type varchar(255) default NULL, + body varchar(4096) default NULL +); + +CREATE TABLE comments ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + post_id int default NULL, + type varchar(255) default NULL, + body varchar(4096) default NULL +); + +CREATE TABLE authors ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + name varchar(255) default NULL +); + +CREATE TABLE tasks ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + starting datetime default NULL, + ending datetime default NULL +); + +CREATE TABLE categories ( + id int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + name varchar(255), + type varchar(255) default NULL +); + +CREATE TABLE categories_posts ( + category_id int NOT NULL, + post_id int NOT NULL +); + +CREATE TABLE fk_test_has_pk ( + id INTEGER NOT NULL PRIMARY KEY +); + +CREATE TABLE fk_test_has_fk ( + id INTEGER NOT NULL PRIMARY KEY, + fk_id INTEGER NOT NULL, + + FOREIGN KEY (fk_id) REFERENCES fk_test_has_pk(id) +); + +CREATE TABLE keyboards ( + key_number int NOT NULL IDENTITY(1, 1) PRIMARY KEY, + name varchar(50) default NULL +); + +--This table has an altered lock_version column name. +CREATE TABLE legacy_things ( + id int NOT NULL IDENTITY(1, 1), + tps_report_number int default NULL, + version int default 0, + PRIMARY KEY (id) +); diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver2.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver2.drop.sql new file mode 100644 index 00000000..df00ffd7 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver2.drop.sql @@ -0,0 +1,2 @@ +DROP TABLE courses; + diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver2.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver2.sql new file mode 100644 index 00000000..9198cf5f --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver2.sql @@ -0,0 +1,5 @@ +CREATE TABLE courses ( + id int NOT NULL PRIMARY KEY, + name varchar(255) NOT NULL +); + diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/sybase.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/sybase.drop.sql new file mode 100644 index 00000000..f843a80f --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/sybase.drop.sql @@ -0,0 +1,31 @@ +DROP TABLE accounts +DROP TABLE funny_jokes +DROP TABLE companies +DROP TABLE topics +DROP TABLE developers +DROP TABLE projects +DROP TABLE developers_projects +DROP TABLE customers +DROP TABLE orders +DROP TABLE movies +DROP TABLE subscribers +DROP TABLE booleantests +DROP TABLE auto_id_tests +DROP TABLE entrants +DROP TABLE colnametests +DROP TABLE mixins +DROP TABLE people +DROP TABLE readers +DROP TABLE binaries +DROP TABLE computers +DROP TABLE tasks +DROP TABLE posts +DROP TABLE comments +DROP TABLE authors +DROP TABLE categories +DROP TABLE categories_posts +DROP TABLE fk_test_has_fk +DROP TABLE fk_test_has_pk +DROP TABLE keyboards +DROP TABLE legacy_things +go diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/sybase.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/sybase.sql new file mode 100644 index 00000000..07f67086 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/sybase.sql @@ -0,0 +1,204 @@ +CREATE TABLE accounts ( + id numeric(9,0) IDENTITY PRIMARY KEY, + firm_id int NULL, + credit_limit int NULL +) + +CREATE TABLE funny_jokes ( +id numeric(9,0) IDENTITY PRIMARY KEY, + name varchar(50) NULL +) + +CREATE TABLE companies ( + id numeric(9,0) IDENTITY PRIMARY KEY, + type varchar(50) NULL, + ruby_type varchar(50) NULL, + firm_id int NULL, + name varchar(50) NULL, + client_of int NULL, + rating int default 1 +) + + +CREATE TABLE topics ( + id numeric(9,0) IDENTITY PRIMARY KEY, + title varchar(255) NULL, + author_name varchar(255) NULL, + author_email_address varchar(255) NULL, + written_on datetime NULL, + bonus_time time NULL, + last_read datetime NULL, + content varchar(255) NULL, + approved bit default 1, + replies_count int default 0, + parent_id int NULL, + type varchar(50) NULL +) + +CREATE TABLE developers ( + id numeric(9,0) IDENTITY PRIMARY KEY, + name varchar(100) NULL, + salary int default 70000, + created_at datetime NULL, + updated_at datetime NULL +) + +CREATE TABLE projects ( + id numeric(9,0) IDENTITY PRIMARY KEY, + name varchar(100) NULL, + type varchar(255) NULL +) + +CREATE TABLE developers_projects ( + developer_id int NOT NULL, + project_id int NOT NULL, + joined_on datetime NULL, + access_level smallint default 1 +) + +CREATE TABLE orders ( + id numeric(9,0) IDENTITY PRIMARY KEY, + name varchar(100) NULL, + billing_customer_id int NULL, + shipping_customer_id int NULL +) + +CREATE TABLE customers ( + id numeric(9,0) IDENTITY PRIMARY KEY, + name varchar(100) NULL, + balance int default 0, + address_street varchar(100) NULL, + address_city varchar(100) NULL, + address_country varchar(100) NULL, + gps_location varchar(100) NULL +) + +CREATE TABLE movies ( + movieid numeric(9,0) IDENTITY PRIMARY KEY, + name varchar(100) NULL +) + +CREATE TABLE subscribers ( + nick varchar(100) PRIMARY KEY, + name varchar(100) NULL +) + +CREATE TABLE booleantests ( + id numeric(9,0) IDENTITY PRIMARY KEY, + value int NULL +) + +CREATE TABLE auto_id_tests ( + auto_id numeric(9,0) IDENTITY PRIMARY KEY, + value int NULL +) + +CREATE TABLE entrants ( + id numeric(9,0) IDENTITY PRIMARY KEY, + name varchar(255) NOT NULL, + course_id int NOT NULL +) + +CREATE TABLE colnametests ( + id numeric(9,0) IDENTITY PRIMARY KEY, + [references] int NOT NULL +) + +CREATE TABLE mixins ( + id numeric(9,0) IDENTITY PRIMARY KEY, + parent_id int NULL, + pos int NULL, + created_at datetime NULL, + updated_at datetime NULL, + lft int NULL, + rgt int NULL, + root_id int NULL, + type varchar(40) NULL +) + +CREATE TABLE people ( + id numeric(9,0) IDENTITY PRIMARY KEY, + first_name varchar(40) NOT NULL, + lock_version int DEFAULT 0 +) + +CREATE TABLE readers ( + id numeric(9,0) IDENTITY PRIMARY KEY, + post_id int NOT NULL, + person_id int NOT NULL +) + +CREATE TABLE binaries ( + id numeric(9,0) IDENTITY PRIMARY KEY, + data image NULL +) + +CREATE TABLE computers ( + id numeric(9,0) IDENTITY PRIMARY KEY, + developer int NOT NULL, + extendedWarranty int NOT NULL +) + +CREATE TABLE posts ( + id numeric(9,0) IDENTITY PRIMARY KEY, + author_id int NULL, + title varchar(255) NOT NULL, + body varchar(2048) NOT NULL, + type varchar(255) NOT NULL +) + +CREATE TABLE comments ( + id numeric(9,0) IDENTITY PRIMARY KEY, + post_id int NOT NULL, + body varchar(2048) NOT NULL, + type varchar(255) NOT NULL +) + +CREATE TABLE authors ( + id numeric(9,0) IDENTITY PRIMARY KEY, + name varchar(255) NOT NULL +) + +CREATE TABLE tasks ( + id numeric(9,0) IDENTITY PRIMARY KEY, + starting datetime NULL, + ending datetime NULL +) + +CREATE TABLE categories ( + id numeric(9,0) IDENTITY PRIMARY KEY, + name varchar(255) NOT NULL, + type varchar(255) NOT NULL +) + +CREATE TABLE categories_posts ( + category_id int NOT NULL, + post_id int NOT NULL +) + +CREATE TABLE fk_test_has_pk ( + id numeric(9,0) IDENTITY PRIMARY KEY +) + +CREATE TABLE fk_test_has_fk ( + id numeric(9,0) PRIMARY KEY, + fk_id numeric(9,0) NOT NULL, + + FOREIGN KEY (fk_id) REFERENCES fk_test_has_pk(id) +) + + +CREATE TABLE keyboards ( + key_number numeric(9,0) IDENTITY PRIMARY KEY, + name varchar(50) NULL +) + +--This table has an altered lock_version column name. +CREATE TABLE legacy_things ( + id numeric(9,0) IDENTITY PRIMARY KEY, + tps_report_number int default NULL, + version int default 0, +) + +go + diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/sybase2.drop.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/sybase2.drop.sql new file mode 100644 index 00000000..f4ce96fe --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/sybase2.drop.sql @@ -0,0 +1,4 @@ +DROP TABLE courses +go + + diff --git a/vendor/rails/activerecord/test/fixtures/db_definitions/sybase2.sql b/vendor/rails/activerecord/test/fixtures/db_definitions/sybase2.sql new file mode 100644 index 00000000..88f9d329 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/db_definitions/sybase2.sql @@ -0,0 +1,5 @@ +CREATE TABLE courses ( + id int NOT NULL PRIMARY KEY, + name varchar(255) NOT NULL +) +go diff --git a/vendor/rails/activerecord/test/fixtures/default.rb b/vendor/rails/activerecord/test/fixtures/default.rb new file mode 100644 index 00000000..887e9cc9 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/default.rb @@ -0,0 +1,2 @@ +class Default < ActiveRecord::Base +end diff --git a/vendor/rails/activerecord/test/fixtures/developer.rb b/vendor/rails/activerecord/test/fixtures/developer.rb new file mode 100644 index 00000000..29555d92 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/developer.rb @@ -0,0 +1,40 @@ +module DeveloperProjectsAssociationExtension + def find_most_recent + find(:first, :order => "id DESC") + end +end + +class Developer < ActiveRecord::Base + has_and_belongs_to_many :projects do + def find_most_recent + find(:first, :order => "id DESC") + end + end + + has_and_belongs_to_many :projects_extended_by_name, + :class_name => "Project", + :join_table => "developers_projects", + :association_foreign_key => "project_id", + :extend => DeveloperProjectsAssociationExtension + + has_and_belongs_to_many :special_projects, :join_table => 'developers_projects', :association_foreign_key => 'project_id' + + validates_inclusion_of :salary, :in => 50000..200000 + validates_length_of :name, :within => 3..20 +end + +DeveloperSalary = Struct.new(:amount) +class DeveloperWithAggregate < ActiveRecord::Base + self.table_name = 'developers' + composed_of :salary, :class_name => 'DeveloperSalary', :mapping => [%w(salary amount)] +end + +class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base + self.table_name = 'developers' + has_and_belongs_to_many :projects, :join_table => 'developers_projects', :foreign_key => 'developer_id' + before_destroy :raise_if_projects_empty! + + def raise_if_projects_empty! + raise if projects.empty? + end +end diff --git a/vendor/rails/activerecord/test/fixtures/developers.yml b/vendor/rails/activerecord/test/fixtures/developers.yml new file mode 100644 index 00000000..308bf75d --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/developers.yml @@ -0,0 +1,21 @@ +david: + id: 1 + name: David + salary: 80000 + +jamis: + id: 2 + name: Jamis + salary: 150000 + +<% for digit in 3..10 %> +dev_<%= digit %>: + id: <%= digit %> + name: fixture_<%= digit %> + salary: 100000 +<% end %> + +poor_jamis: + id: 11 + name: Jamis + salary: 9000 \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/developers_projects.yml b/vendor/rails/activerecord/test/fixtures/developers_projects.yml new file mode 100644 index 00000000..57295870 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/developers_projects.yml @@ -0,0 +1,17 @@ +david_action_controller: + developer_id: 1 + project_id: 2 + joined_on: 2004-10-10 + +david_active_record: + developer_id: 1 + project_id: 1 + joined_on: 2004-10-10 + +jamis_active_record: + developer_id: 2 + project_id: 1 + +poor_jamis_active_record: + developer_id: 11 + project_id: 1 \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/developers_projects/david_action_controller b/vendor/rails/activerecord/test/fixtures/developers_projects/david_action_controller new file mode 100644 index 00000000..e6e9d0e5 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/developers_projects/david_action_controller @@ -0,0 +1,3 @@ +developer_id => 1 +project_id => 2 +joined_on => 2004-10-10 \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/developers_projects/david_active_record b/vendor/rails/activerecord/test/fixtures/developers_projects/david_active_record new file mode 100644 index 00000000..2ef474c1 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/developers_projects/david_active_record @@ -0,0 +1,3 @@ +developer_id => 1 +project_id => 1 +joined_on => 2004-10-10 \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/developers_projects/jamis_active_record b/vendor/rails/activerecord/test/fixtures/developers_projects/jamis_active_record new file mode 100644 index 00000000..91beb807 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/developers_projects/jamis_active_record @@ -0,0 +1,2 @@ +developer_id => 2 +project_id => 1 \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/entrant.rb b/vendor/rails/activerecord/test/fixtures/entrant.rb new file mode 100644 index 00000000..4682ce48 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/entrant.rb @@ -0,0 +1,3 @@ +class Entrant < ActiveRecord::Base + belongs_to :course +end diff --git a/vendor/rails/activerecord/test/fixtures/entrants.yml b/vendor/rails/activerecord/test/fixtures/entrants.yml new file mode 100644 index 00000000..86f0108e --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/entrants.yml @@ -0,0 +1,14 @@ +first: + id: 1 + course_id: 1 + name: Ruby Developer + +second: + id: 2 + course_id: 1 + name: Ruby Guru + +third: + id: 3 + course_id: 2 + name: Java Lover diff --git a/vendor/rails/activerecord/test/fixtures/fk_test_has_fk.yml b/vendor/rails/activerecord/test/fixtures/fk_test_has_fk.yml new file mode 100644 index 00000000..67d914e1 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/fk_test_has_fk.yml @@ -0,0 +1,3 @@ +first: + id: 1 + fk_id: 1 diff --git a/vendor/rails/activerecord/test/fixtures/fk_test_has_pk.yml b/vendor/rails/activerecord/test/fixtures/fk_test_has_pk.yml new file mode 100644 index 00000000..c9395218 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/fk_test_has_pk.yml @@ -0,0 +1,2 @@ +first: + id: 1 \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/flowers.jpg b/vendor/rails/activerecord/test/fixtures/flowers.jpg new file mode 100644 index 00000000..3687b097 Binary files /dev/null and b/vendor/rails/activerecord/test/fixtures/flowers.jpg differ diff --git a/vendor/rails/activerecord/test/fixtures/funny_jokes.yml b/vendor/rails/activerecord/test/fixtures/funny_jokes.yml new file mode 100644 index 00000000..834481bc --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/funny_jokes.yml @@ -0,0 +1,14 @@ +a_joke: + id: 1 + name: Knock knock + +another_joke: + id: 2 + name: The Aristocrats +a_joke: + id: 1 + name: Knock knock + +another_joke: + id: 2 + name: The Aristocrats \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/joke.rb b/vendor/rails/activerecord/test/fixtures/joke.rb new file mode 100644 index 00000000..8006a43b --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/joke.rb @@ -0,0 +1,6 @@ +class Joke < ActiveRecord::Base + set_table_name 'funny_jokes' +end +class Joke < ActiveRecord::Base + set_table_name 'funny_jokes' +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/keyboard.rb b/vendor/rails/activerecord/test/fixtures/keyboard.rb new file mode 100644 index 00000000..32a4a7fa --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/keyboard.rb @@ -0,0 +1,3 @@ +class Keyboard < ActiveRecord::Base + set_primary_key 'key_number' +end diff --git a/vendor/rails/activerecord/test/fixtures/legacy_thing.rb b/vendor/rails/activerecord/test/fixtures/legacy_thing.rb new file mode 100644 index 00000000..eaeb642d --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/legacy_thing.rb @@ -0,0 +1,3 @@ +class LegacyThing < ActiveRecord::Base + set_locking_column :version +end diff --git a/vendor/rails/activerecord/test/fixtures/legacy_things.yml b/vendor/rails/activerecord/test/fixtures/legacy_things.yml new file mode 100644 index 00000000..a6d42aab --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/legacy_things.yml @@ -0,0 +1,3 @@ +obtuse: + id: 1 + tps_report_number: 500 diff --git a/vendor/rails/activerecord/test/fixtures/migrations/1_people_have_last_names.rb b/vendor/rails/activerecord/test/fixtures/migrations/1_people_have_last_names.rb new file mode 100644 index 00000000..009729b3 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/migrations/1_people_have_last_names.rb @@ -0,0 +1,9 @@ +class PeopleHaveLastNames < ActiveRecord::Migration + def self.up + add_column "people", "last_name", :string + end + + def self.down + remove_column "people", "last_name" + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/migrations/2_we_need_reminders.rb b/vendor/rails/activerecord/test/fixtures/migrations/2_we_need_reminders.rb new file mode 100644 index 00000000..ac5918f0 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/migrations/2_we_need_reminders.rb @@ -0,0 +1,12 @@ +class WeNeedReminders < ActiveRecord::Migration + def self.up + create_table("reminders") do |t| + t.column :content, :text + t.column :remind_at, :datetime + end + end + + def self.down + drop_table "reminders" + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/migrations/3_innocent_jointable.rb b/vendor/rails/activerecord/test/fixtures/migrations/3_innocent_jointable.rb new file mode 100644 index 00000000..21c9ca53 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/migrations/3_innocent_jointable.rb @@ -0,0 +1,12 @@ +class InnocentJointable < ActiveRecord::Migration + def self.up + create_table("people_reminders", :id => false) do |t| + t.column :reminder_id, :integer + t.column :person_id, :integer + end + end + + def self.down + drop_table "people_reminders" + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/1_people_have_last_names.rb b/vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/1_people_have_last_names.rb new file mode 100644 index 00000000..009729b3 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/1_people_have_last_names.rb @@ -0,0 +1,9 @@ +class PeopleHaveLastNames < ActiveRecord::Migration + def self.up + add_column "people", "last_name", :string + end + + def self.down + remove_column "people", "last_name" + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/2_we_need_reminders.rb b/vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/2_we_need_reminders.rb new file mode 100644 index 00000000..ac5918f0 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/2_we_need_reminders.rb @@ -0,0 +1,12 @@ +class WeNeedReminders < ActiveRecord::Migration + def self.up + create_table("reminders") do |t| + t.column :content, :text + t.column :remind_at, :datetime + end + end + + def self.down + drop_table "reminders" + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/3_foo.rb b/vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/3_foo.rb new file mode 100644 index 00000000..916fe580 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/3_foo.rb @@ -0,0 +1,7 @@ +class Foo < ActiveRecord::Migration + def self.up + end + + def self.down + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/3_innocent_jointable.rb b/vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/3_innocent_jointable.rb new file mode 100644 index 00000000..21c9ca53 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/3_innocent_jointable.rb @@ -0,0 +1,12 @@ +class InnocentJointable < ActiveRecord::Migration + def self.up + create_table("people_reminders", :id => false) do |t| + t.column :reminder_id, :integer + t.column :person_id, :integer + end + end + + def self.down + drop_table "people_reminders" + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/mixin.rb b/vendor/rails/activerecord/test/fixtures/mixin.rb new file mode 100644 index 00000000..78cdbef9 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/mixin.rb @@ -0,0 +1,48 @@ +class Mixin < ActiveRecord::Base + +end + +class TreeMixin < Mixin + acts_as_tree :foreign_key => "parent_id", :order => "id" +end + +class TreeMixinWithoutOrder < Mixin + acts_as_tree :foreign_key => "parent_id" +end + +class ListMixin < Mixin + acts_as_list :column => "pos", :scope => :parent + + def self.table_name() "mixins" end +end + +class ListMixinSub1 < ListMixin +end + +class ListMixinSub2 < ListMixin +end + + +class ListWithStringScopeMixin < ActiveRecord::Base + acts_as_list :column => "pos", :scope => 'parent_id = #{parent_id}' + + def self.table_name() "mixins" end +end + +class NestedSet < Mixin + acts_as_nested_set :scope => "root_id IS NULL" + + def self.table_name() "mixins" end +end + +class NestedSetWithStringScope < Mixin + acts_as_nested_set :scope => 'root_id = #{root_id}' + + def self.table_name() "mixins" end +end + +class NestedSetWithSymbolScope < Mixin + acts_as_nested_set :scope => :root + + def self.table_name() "mixins" end +end diff --git a/vendor/rails/activerecord/test/fixtures/mixins.yml b/vendor/rails/activerecord/test/fixtures/mixins.yml new file mode 100644 index 00000000..cb21349c --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/mixins.yml @@ -0,0 +1,89 @@ +# tree mixins +tree_1: + id: 1001 + type: TreeMixin + parent_id: + +tree_2: + id: 1002 + type: TreeMixin + parent_id: 1001 + +tree_3: + id: 1003 + type: TreeMixin + parent_id: 1002 + +tree_4: + id: 1004 + type: TreeMixin + parent_id: 1001 + +tree2_1: + id: 1005 + type: TreeMixin + parent_id: + +tree3_1: + id: 1006 + type: TreeMixin + parent_id: + +tree_without_order_1: + id: 1101 + type: TreeMixinWithoutOrder + parent_id: + +tree_without_order_2: + id: 1100 + type: TreeMixinWithoutOrder + parent_id: + +# List mixins + +<% (1..4).each do |counter| %> +list_<%= counter %>: + id: <%= counter+1006 %> + pos: <%= counter %> + type: ListMixin + parent_id: 5 +<% end %> + +# Nested set mixins + +<% (1..10).each do |counter| %> +set_<%= counter %>: + id: <%= counter+3000 %> + type: NestedSet +<% end %> + +# Big old set +<% +[[4001, 0, 1, 20], + [4002, 4001, 2, 7], + [4003, 4002, 3, 4], + [4004, 4002, 5, 6], + [4005, 4001, 8, 13], + [4006, 4005, 9, 10], + [4007, 4005, 11, 12], + [4008, 4001, 14, 19], + [4009, 4008, 15, 16], + [4010, 4008, 17, 18]].each do |set| %> +tree_<%= set[0] %>: + id: <%= set[0]%> + parent_id: <%= set[1]%> + type: NestedSetWithStringScope + lft: <%= set[2]%> + rgt: <%= set[3]%> + root_id: 42 + +<% end %> + +# subclasses of list items +<% (1..4).each do |i| %> +list_sub_<%= i %>: + id: <%= i + 5000 %> + pos: <%= i %> + parent_id: 5000 + type: <%= (i % 2 == 1) ? ListMixinSub1 : ListMixinSub2 %> +<% end %> diff --git a/vendor/rails/activerecord/test/fixtures/movie.rb b/vendor/rails/activerecord/test/fixtures/movie.rb new file mode 100644 index 00000000..6384b4c8 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/movie.rb @@ -0,0 +1,5 @@ +class Movie < ActiveRecord::Base + def self.primary_key + "movieid" + end +end diff --git a/vendor/rails/activerecord/test/fixtures/movies.yml b/vendor/rails/activerecord/test/fixtures/movies.yml new file mode 100644 index 00000000..2e9154fd --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/movies.yml @@ -0,0 +1,7 @@ +first: + movieid: 1 + name: Terminator + +second: + movieid: 2 + name: Gladiator diff --git a/vendor/rails/activerecord/test/fixtures/naked/csv/accounts.csv b/vendor/rails/activerecord/test/fixtures/naked/csv/accounts.csv new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/naked/csv/accounts.csv @@ -0,0 +1 @@ + diff --git a/vendor/rails/activerecord/test/fixtures/naked/yml/accounts.yml b/vendor/rails/activerecord/test/fixtures/naked/yml/accounts.yml new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/naked/yml/accounts.yml @@ -0,0 +1 @@ + diff --git a/vendor/rails/activerecord/test/fixtures/naked/yml/companies.yml b/vendor/rails/activerecord/test/fixtures/naked/yml/companies.yml new file mode 100644 index 00000000..2c151c20 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/naked/yml/companies.yml @@ -0,0 +1 @@ +# i wonder what will happen here diff --git a/vendor/rails/activerecord/test/fixtures/naked/yml/courses.yml b/vendor/rails/activerecord/test/fixtures/naked/yml/courses.yml new file mode 100644 index 00000000..19f0805d --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/naked/yml/courses.yml @@ -0,0 +1 @@ +qwerty diff --git a/vendor/rails/activerecord/test/fixtures/order.rb b/vendor/rails/activerecord/test/fixtures/order.rb new file mode 100644 index 00000000..ba114f22 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/order.rb @@ -0,0 +1,4 @@ +class Order < ActiveRecord::Base + belongs_to :billing, :class_name => 'Customer', :foreign_key => 'billing_customer_id' + belongs_to :shipping, :class_name => 'Customer', :foreign_key => 'shipping_customer_id' +end diff --git a/vendor/rails/activerecord/test/fixtures/people.yml b/vendor/rails/activerecord/test/fixtures/people.yml new file mode 100644 index 00000000..22c64afb --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/people.yml @@ -0,0 +1,3 @@ +michael: + id: 1 + first_name: Michael \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/person.rb b/vendor/rails/activerecord/test/fixtures/person.rb new file mode 100644 index 00000000..7a9666f4 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/person.rb @@ -0,0 +1,4 @@ +class Person < ActiveRecord::Base + has_many :readers + has_many :posts, :through => :readers +end diff --git a/vendor/rails/activerecord/test/fixtures/post.rb b/vendor/rails/activerecord/test/fixtures/post.rb new file mode 100644 index 00000000..9b42fbdb --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/post.rb @@ -0,0 +1,57 @@ +class Post < ActiveRecord::Base + belongs_to :author do + def greeting + "hello" + end + end + + belongs_to :author_with_posts, :class_name => "Author", :include => :posts + + has_many :comments, :order => "body" do + def find_most_recent + find(:first, :order => "id DESC") + end + end + + has_one :very_special_comment + has_one :very_special_comment_with_post, :class_name => "VerySpecialComment", :include => :post + has_many :special_comments + + has_and_belongs_to_many :categories + has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id' + + has_many :taggings, :as => :taggable + has_many :tags, :through => :taggings, :include => :tagging do + def add_joins_and_select + find :all, :select => 'tags.*, authors.id as author_id', :include => false, + :joins => 'left outer join posts on taggings.taggable_id = posts.id left outer join authors on posts.author_id = authors.id' + end + end + + has_many :funky_tags, :through => :taggings, :source => :tag + has_many :super_tags, :through => :taggings + has_one :tagging, :as => :taggable + + has_many :invalid_taggings, :as => :taggable, :class_name => "Tagging", :conditions => 'taggings.id < 0' + has_many :invalid_tags, :through => :invalid_taggings, :source => :tag + + has_many :categorizations, :foreign_key => :category_id + has_many :authors, :through => :categorizations + + has_many :readers + has_many :people, :through => :readers + + def self.what_are_you + 'a post...' + end +end + +class SpecialPost < Post; end; + +class StiPost < Post + self.abstract_class = true + has_one :special_comment, :class_name => "SpecialComment" +end + +class SubStiPost < StiPost +end diff --git a/vendor/rails/activerecord/test/fixtures/posts.yml b/vendor/rails/activerecord/test/fixtures/posts.yml new file mode 100644 index 00000000..0f1445b6 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/posts.yml @@ -0,0 +1,48 @@ +welcome: + id: 1 + author_id: 1 + title: Welcome to the weblog + body: Such a lovely day + type: Post + +thinking: + id: 2 + author_id: 1 + title: So I was thinking + body: Like I hopefully always am + type: SpecialPost + +authorless: + id: 3 + author_id: 0 + title: I don't have any comments + body: I just don't want to + type: Post + +sti_comments: + id: 4 + author_id: 1 + title: sti comments + body: hello + type: Post + +sti_post_and_comments: + id: 5 + author_id: 1 + title: sti me + body: hello + type: StiPost + +sti_habtm: + id: 6 + author_id: 1 + title: habtm sti test + body: hello + type: Post + +eager_other: + id: 7 + author_id: 2 + title: eager loading with OR'd conditions + body: hello + type: Post diff --git a/vendor/rails/activerecord/test/fixtures/project.rb b/vendor/rails/activerecord/test/fixtures/project.rb new file mode 100644 index 00000000..c1aa4145 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/project.rb @@ -0,0 +1,25 @@ +class Project < ActiveRecord::Base + has_and_belongs_to_many :developers, :uniq => true + has_and_belongs_to_many :limited_developers, :class_name => "Developer", :limit => 1 + has_and_belongs_to_many :developers_named_david, :class_name => "Developer", :conditions => "name = 'David'", :uniq => true + has_and_belongs_to_many :salaried_developers, :class_name => "Developer", :conditions => "salary > 0" + has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => 'SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id}' + has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => "DELETE FROM developers_projects WHERE project_id = \#{id} AND developer_id = \#{record.id}" + has_and_belongs_to_many :developers_with_callbacks, :class_name => "Developer", :before_add => Proc.new {|o, r| o.developers_log << "before_adding#{r.id}"}, + :after_add => Proc.new {|o, r| o.developers_log << "after_adding#{r.id}"}, + :before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"}, + :after_remove => Proc.new {|o, r| o.developers_log << "after_removing#{r.id}"} + + attr_accessor :developers_log + + def after_initialize + @developers_log = [] + end + +end + +class SpecialProject < Project + def hello_world + "hello there!" + end +end diff --git a/vendor/rails/activerecord/test/fixtures/projects.yml b/vendor/rails/activerecord/test/fixtures/projects.yml new file mode 100644 index 00000000..02800c78 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/projects.yml @@ -0,0 +1,7 @@ +action_controller: + id: 2 + name: Active Controller + +active_record: + id: 1 + name: Active Record diff --git a/vendor/rails/activerecord/test/fixtures/reader.rb b/vendor/rails/activerecord/test/fixtures/reader.rb new file mode 100644 index 00000000..27527bf5 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/reader.rb @@ -0,0 +1,4 @@ +class Reader < ActiveRecord::Base + belongs_to :post + belongs_to :person +end diff --git a/vendor/rails/activerecord/test/fixtures/readers.yml b/vendor/rails/activerecord/test/fixtures/readers.yml new file mode 100644 index 00000000..6ed73c9e --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/readers.yml @@ -0,0 +1,4 @@ +michael_welcome: + id: 1 + post_id: 1 + person_id: 1 diff --git a/vendor/rails/activerecord/test/fixtures/reply.rb b/vendor/rails/activerecord/test/fixtures/reply.rb new file mode 100755 index 00000000..bf7781e8 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/reply.rb @@ -0,0 +1,37 @@ +require 'fixtures/topic' + +class Reply < Topic + belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true + has_many :replies, :class_name => "SillyReply", :dependent => :destroy, :foreign_key => "parent_id" + + validate :errors_on_empty_content + validate_on_create :title_is_wrong_create + + attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read + + def validate + errors.add("title", "Empty") unless attribute_present? "title" + end + + def errors_on_empty_content + errors.add("content", "Empty") unless attribute_present? "content" + end + + def validate_on_create + if attribute_present?("title") && attribute_present?("content") && content == "Mismatch" + errors.add("title", "is Content Mismatch") + end + end + + def title_is_wrong_create + errors.add("title", "is Wrong Create") if attribute_present?("title") && title == "Wrong Create" + end + + def validate_on_update + errors.add("title", "is Wrong Update") if attribute_present?("title") && title == "Wrong Update" + end +end + +class SillyReply < Reply + belongs_to :reply, :foreign_key => "parent_id", :counter_cache => :replies_count +end diff --git a/vendor/rails/activerecord/test/fixtures/subject.rb b/vendor/rails/activerecord/test/fixtures/subject.rb new file mode 100644 index 00000000..3502943f --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/subject.rb @@ -0,0 +1,4 @@ +# used for OracleSynonymTest, see test/synonym_test_oci.rb +# +class Subject < ActiveRecord::Base +end diff --git a/vendor/rails/activerecord/test/fixtures/subscriber.rb b/vendor/rails/activerecord/test/fixtures/subscriber.rb new file mode 100644 index 00000000..51335a8f --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/subscriber.rb @@ -0,0 +1,6 @@ +class Subscriber < ActiveRecord::Base + set_primary_key 'nick' +end + +class SpecialSubscriber < Subscriber +end diff --git a/vendor/rails/activerecord/test/fixtures/subscribers/first b/vendor/rails/activerecord/test/fixtures/subscribers/first new file mode 100644 index 00000000..5287e26e --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/subscribers/first @@ -0,0 +1,2 @@ +nick => alterself +name => Luke Holden diff --git a/vendor/rails/activerecord/test/fixtures/subscribers/second b/vendor/rails/activerecord/test/fixtures/subscribers/second new file mode 100644 index 00000000..2345e447 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/subscribers/second @@ -0,0 +1,2 @@ +nick => webster132 +name => David Heinemeier Hansson diff --git a/vendor/rails/activerecord/test/fixtures/tag.rb b/vendor/rails/activerecord/test/fixtures/tag.rb new file mode 100644 index 00000000..c12ec0c1 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/tag.rb @@ -0,0 +1,5 @@ +class Tag < ActiveRecord::Base + has_many :taggings + has_many :taggables, :through => :taggings + has_one :tagging +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/tagging.rb b/vendor/rails/activerecord/test/fixtures/tagging.rb new file mode 100644 index 00000000..4695f075 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/tagging.rb @@ -0,0 +1,6 @@ +class Tagging < ActiveRecord::Base + belongs_to :tag, :include => :tagging + belongs_to :super_tag, :class_name => 'Tag', :foreign_key => 'super_tag_id' + belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id' + belongs_to :taggable, :polymorphic => true, :counter_cache => true +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/taggings.yml b/vendor/rails/activerecord/test/fixtures/taggings.yml new file mode 100644 index 00000000..617210d6 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/taggings.yml @@ -0,0 +1,18 @@ +welcome_general: + id: 1 + tag_id: 1 + super_tag_id: 2 + taggable_id: 1 + taggable_type: Post + +thinking_general: + id: 2 + tag_id: 1 + taggable_id: 2 + taggable_type: Post + +fake: + id: 3 + tag_id: 1 + taggable_id: 1 + taggable_type: FakeModel \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/tags.yml b/vendor/rails/activerecord/test/fixtures/tags.yml new file mode 100644 index 00000000..471b96f3 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/tags.yml @@ -0,0 +1,7 @@ +general: + id: 1 + name: General + +misc: + id: 2 + name: Misc \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/task.rb b/vendor/rails/activerecord/test/fixtures/task.rb new file mode 100644 index 00000000..ee0282c7 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/task.rb @@ -0,0 +1,3 @@ +class Task < ActiveRecord::Base + +end diff --git a/vendor/rails/activerecord/test/fixtures/tasks.yml b/vendor/rails/activerecord/test/fixtures/tasks.yml new file mode 100644 index 00000000..1e6a061a --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/tasks.yml @@ -0,0 +1,7 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +first_task: + id: 1 + starting: 2005-03-30t06:30:00.00+01:00 + ending: 2005-03-30t08:30:00.00+01:00 +another_task: + id: 2 diff --git a/vendor/rails/activerecord/test/fixtures/topic.rb b/vendor/rails/activerecord/test/fixtures/topic.rb new file mode 100755 index 00000000..9b20f02c --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/topic.rb @@ -0,0 +1,20 @@ +class Topic < ActiveRecord::Base + has_many :replies, :dependent => :destroy, :foreign_key => "parent_id" + serialize :content + + before_create :default_written_on + before_destroy :destroy_children + + def parent + Topic.find(parent_id) + end + + protected + def default_written_on + self.written_on = Time.now unless attribute_present?("written_on") + end + + def destroy_children + self.class.delete_all "parent_id = #{id}" + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/fixtures/topics.yml b/vendor/rails/activerecord/test/fixtures/topics.yml new file mode 100644 index 00000000..810bbcf4 --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures/topics.yml @@ -0,0 +1,22 @@ +first: + id: 1 + title: The First Topic + author_name: David + author_email_address: david@loudthinking.com + written_on: 2003-07-16t15:28:00.00+01:00 + last_read: 2004-04-15 + bonus_time: 2005-01-30t15:28:00.00+01:00 + content: Have a nice day + approved: false + replies_count: 0 + +second: + id: 2 + title: The Second Topic's of the day + author_name: Mary + written_on: 2003-07-15t15:28:00.00+01:00 + content: Have a nice day + approved: true + replies_count: 2 + parent_id: 1 + type: Reply diff --git a/vendor/rails/activerecord/test/fixtures_test.rb b/vendor/rails/activerecord/test/fixtures_test.rb new file mode 100755 index 00000000..88f01c8a --- /dev/null +++ b/vendor/rails/activerecord/test/fixtures_test.rb @@ -0,0 +1,345 @@ +require 'abstract_unit' +require 'fixtures/topic' +require 'fixtures/developer' +require 'fixtures/company' +require 'fixtures/task' +require 'fixtures/reply' +require 'fixtures/joke' +require 'fixtures/category' + +class FixturesTest < Test::Unit::TestCase + self.use_instantiated_fixtures = true + self.use_transactional_fixtures = false + + fixtures :topics, :developers, :accounts, :tasks, :categories, :funny_jokes + + FIXTURES = %w( accounts companies customers + developers developers_projects entrants + movies projects subscribers topics tasks ) + MATCH_ATTRIBUTE_NAME = /[a-zA-Z][-_\w]*/ + + def test_clean_fixtures + FIXTURES.each do |name| + fixtures = nil + assert_nothing_raised { fixtures = create_fixtures(name) } + assert_kind_of(Fixtures, fixtures) + fixtures.each { |name, fixture| + fixture.each { |key, value| + assert_match(MATCH_ATTRIBUTE_NAME, key) + } + } + end + end + + def test_multiple_clean_fixtures + fixtures_array = nil + assert_nothing_raised { fixtures_array = create_fixtures(*FIXTURES) } + assert_kind_of(Array, fixtures_array) + fixtures_array.each { |fixtures| assert_kind_of(Fixtures, fixtures) } + end + + def test_attributes + topics = create_fixtures("topics") + assert_equal("The First Topic", topics["first"]["title"]) + assert_nil(topics["second"]["author_email_address"]) + end + + def test_inserts + topics = create_fixtures("topics") + firstRow = ActiveRecord::Base.connection.select_one("SELECT * FROM topics WHERE author_name = 'David'") + assert_equal("The First Topic", firstRow["title"]) + + secondRow = ActiveRecord::Base.connection.select_one("SELECT * FROM topics WHERE author_name = 'Mary'") + assert_nil(secondRow["author_email_address"]) + end + + if ActiveRecord::Base.connection.supports_migrations? + def test_inserts_with_pre_and_suffix + ActiveRecord::Base.connection.create_table :prefix_topics_suffix do |t| + t.column :title, :string + t.column :author_name, :string + t.column :author_email_address, :string + t.column :written_on, :datetime + t.column :bonus_time, :time + t.column :last_read, :date + t.column :content, :string + t.column :approved, :boolean, :default => true + t.column :replies_count, :integer, :default => 0 + t.column :parent_id, :integer + t.column :type, :string, :limit => 50 + end + + # Store existing prefix/suffix + old_prefix = ActiveRecord::Base.table_name_prefix + old_suffix = ActiveRecord::Base.table_name_suffix + + # Set a prefix/suffix we can test against + ActiveRecord::Base.table_name_prefix = 'prefix_' + ActiveRecord::Base.table_name_suffix = '_suffix' + + topics = create_fixtures("topics") + + firstRow = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_topics_suffix WHERE author_name = 'David'") + assert_equal("The First Topic", firstRow["title"]) + + secondRow = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_topics_suffix WHERE author_name = 'Mary'") + assert_nil(secondRow["author_email_address"]) + ensure + # Restore prefix/suffix to its previous values + ActiveRecord::Base.table_name_prefix = old_prefix + ActiveRecord::Base.table_name_suffix = old_suffix + + ActiveRecord::Base.connection.drop_table :prefix_topics_suffix rescue nil + end + end + + def test_insert_with_datetime + topics = create_fixtures("tasks") + first = Task.find(1) + assert first + end + + + def test_bad_format + path = File.join(File.dirname(__FILE__), 'fixtures', 'bad_fixtures') + Dir.entries(path).each do |file| + next unless File.file?(file) and file !~ Fixtures::DEFAULT_FILTER_RE + assert_raise(Fixture::FormatError) { + Fixture.new(bad_fixtures_path, file) + } + end + end + + def test_deprecated_yaml_extension + assert_raise(Fixture::FormatError) { + Fixtures.new(nil, 'bad_extension', 'BadExtension', File.join(File.dirname(__FILE__), 'fixtures')) + } + end + + def test_logger_level_invariant + level = ActiveRecord::Base.logger.level + create_fixtures('topics') + assert_equal level, ActiveRecord::Base.logger.level + end + + def test_instantiation + topics = create_fixtures("topics") + assert_kind_of Topic, topics["first"].find + end + + def test_complete_instantiation + assert_equal 2, @topics.size + assert_equal "The First Topic", @first.title + end + + def test_fixtures_from_root_yml_with_instantiation + # assert_equal 2, @accounts.size + assert_equal 50, @unknown.credit_limit + end + + def test_erb_in_fixtures + assert_equal 11, @developers.size + assert_equal "fixture_5", @dev_5.name + end + + def test_empty_yaml_fixture + assert_not_nil Fixtures.new( Account.connection, "accounts", 'Account', File.dirname(__FILE__) + "/fixtures/naked/yml/accounts") + end + + def test_empty_yaml_fixture_with_a_comment_in_it + assert_not_nil Fixtures.new( Account.connection, "companies", 'Company', File.dirname(__FILE__) + "/fixtures/naked/yml/companies") + end + + def test_dirty_dirty_yaml_file + assert_raises(Fixture::FormatError) do + Fixtures.new( Account.connection, "courses", 'Course', File.dirname(__FILE__) + "/fixtures/naked/yml/courses") + end + end + + def test_empty_csv_fixtures + assert_not_nil Fixtures.new( Account.connection, "accounts", 'Account', File.dirname(__FILE__) + "/fixtures/naked/csv/accounts") + end + + def test_omap_fixtures + assert_nothing_raised do + fixtures = Fixtures.new(Account.connection, 'categories', 'Category', File.dirname(__FILE__) + '/fixtures/categories_ordered') + + i = 0 + fixtures.each do |name, fixture| + assert_equal "fixture_no_#{i}", name + assert_equal "Category #{i}", fixture['name'] + i += 1 + end + end + end + + + 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).class, SpecialCategory) + end + + def test_subsubdir_file_with_arbitrary_name + assert_equal(categories(:sub_special_3).name, "A special category in an arbitrarily named subsubdir file") + assert_equal(categories(:sub_special_3).class, SpecialCategory) + end + + +end + +if Account.connection.respond_to?(:reset_pk_sequence!) + class FixturesResetPkSequenceTest < Test::Unit::TestCase + fixtures :accounts + fixtures :companies + + def setup + @instances = [Account.new(:credit_limit => 50), Company.new(:name => 'RoR Consulting')] + end + + def test_resets_to_min_pk_with_specified_pk_and_sequence + @instances.each do |instance| + model = instance.class + model.delete_all + model.connection.reset_pk_sequence!(model.table_name, model.primary_key, model.sequence_name) + + instance.save! + assert_equal 1, instance.id, "Sequence reset for #{model.table_name} failed." + end + end + + def test_resets_to_min_pk_with_default_pk_and_sequence + @instances.each do |instance| + model = instance.class + model.delete_all + model.connection.reset_pk_sequence!(model.table_name) + + instance.save! + assert_equal 1, instance.id, "Sequence reset for #{model.table_name} failed." + end + end + + def test_create_fixtures_resets_sequences + @instances.each do |instance| + max_id = create_fixtures(instance.class.table_name).inject(0) do |max_id, (name, fixture)| + fixture_id = fixture['id'].to_i + fixture_id > max_id ? fixture_id : max_id + end + + # Clone the last fixture to check that it gets the next greatest id. + instance.save! + assert_equal max_id + 1, instance.id, "Sequence reset for #{instance.class.table_name} failed." + end + end + end +end + + +class FixturesWithoutInstantiationTest < Test::Unit::TestCase + self.use_instantiated_fixtures = false + fixtures :topics, :developers, :accounts + + def test_without_complete_instantiation + assert_nil @first + assert_nil @topics + assert_nil @developers + assert_nil @accounts + end + + def test_fixtures_from_root_yml_without_instantiation + assert_nil @unknown + end + + def test_accessor_methods + assert_equal "The First Topic", topics(:first).title + assert_equal "Jamis", developers(:jamis).name + assert_equal 50, accounts(:signals37).credit_limit + end +end + + +class FixturesWithoutInstanceInstantiationTest < Test::Unit::TestCase + self.use_instantiated_fixtures = true + self.use_instantiated_fixtures = :no_instances + + fixtures :topics, :developers, :accounts + + def test_without_instance_instantiation + assert_nil @first + assert_not_nil @topics + assert_not_nil @developers + assert_not_nil @accounts + end +end + + +class TransactionalFixturesTest < Test::Unit::TestCase + self.use_instantiated_fixtures = true + self.use_transactional_fixtures = true + + fixtures :topics + + def test_destroy + assert_not_nil @first + @first.destroy + end + + def test_destroy_just_kidding + assert_not_nil @first + end +end + + +class MultipleFixturesTest < Test::Unit::TestCase + fixtures :topics + fixtures :developers, :accounts + + def test_fixture_table_names + assert_equal %w(topics developers accounts), fixture_table_names + end +end + + +class OverlappingFixturesTest < Test::Unit::TestCase + fixtures :topics, :developers + fixtures :developers, :accounts + + def test_fixture_table_names + assert_equal %w(topics developers accounts), fixture_table_names + end +end + + +class ForeignKeyFixturesTest < Test::Unit::TestCase + fixtures :fk_test_has_pk, :fk_test_has_fk + + # if foreign keys are implemented and fixtures + # are not deleted in reverse order then this test + # case will raise StatementInvalid + + def test_number1 + assert true + end + + def test_number2 + assert true + end +end + +class SetTableNameFixturesTest < Test::Unit::TestCase + set_fixture_class :funny_jokes => 'Joke' + fixtures :funny_jokes + + def test_table_method + assert_kind_of Joke, funny_jokes(:a_joke) + end +end + +class InvalidTableNameFixturesTest < Test::Unit::TestCase + fixtures :funny_jokes + + def test_raises_error + assert_raises FixtureClassNotFound do + funny_jokes(:a_joke) + end + end +end diff --git a/vendor/rails/activerecord/test/inheritance_test.rb b/vendor/rails/activerecord/test/inheritance_test.rb new file mode 100755 index 00000000..211f7397 --- /dev/null +++ b/vendor/rails/activerecord/test/inheritance_test.rb @@ -0,0 +1,144 @@ +require 'abstract_unit' +require 'fixtures/company' +require 'fixtures/project' +require 'fixtures/subscriber' + +class InheritanceTest < Test::Unit::TestCase + fixtures :companies, :projects, :subscribers + + def test_a_bad_type_column + #SQLServer need to turn Identity Insert On before manually inserting into the Identity column + if current_adapter?(:SQLServerAdapter) || current_adapter?(:SybaseAdapter) + Company.connection.execute "SET IDENTITY_INSERT companies ON" + end + Company.connection.insert "INSERT INTO companies (id, #{QUOTED_TYPE}, name) VALUES(100, 'bad_class!', 'Not happening')" + + #We then need to turn it back Off before continuing. + if current_adapter?(:SQLServerAdapter) || current_adapter?(:SybaseAdapter) + Company.connection.execute "SET IDENTITY_INSERT companies OFF" + end + assert_raises(ActiveRecord::SubclassNotFound) { Company.find(100) } + end + + def test_inheritance_find + assert Company.find(1).kind_of?(Firm), "37signals should be a firm" + assert Firm.find(1).kind_of?(Firm), "37signals should be a firm" + assert Company.find(2).kind_of?(Client), "Summit should be a client" + assert Client.find(2).kind_of?(Client), "Summit should be a client" + end + + def test_alt_inheritance_find + switch_to_alt_inheritance_column + test_inheritance_find + end + + def test_inheritance_find_all + companies = Company.find(:all, :order => 'id') + assert companies[0].kind_of?(Firm), "37signals should be a firm" + assert companies[1].kind_of?(Client), "Summit should be a client" + end + + def test_alt_inheritance_find_all + switch_to_alt_inheritance_column + test_inheritance_find_all + end + + def test_inheritance_save + firm = Firm.new + firm.name = "Next Angle" + firm.save + + next_angle = Company.find(firm.id) + assert next_angle.kind_of?(Firm), "Next Angle should be a firm" + end + + def test_alt_inheritance_save + switch_to_alt_inheritance_column + test_inheritance_save + end + + def test_inheritance_condition + assert_equal 8, Company.count + assert_equal 2, Firm.count + assert_equal 3, Client.count + end + + def test_alt_inheritance_condition + switch_to_alt_inheritance_column + test_inheritance_condition + end + + def test_finding_incorrect_type_data + assert_raises(ActiveRecord::RecordNotFound) { Firm.find(2) } + assert_nothing_raised { Firm.find(1) } + end + + def test_alt_finding_incorrect_type_data + switch_to_alt_inheritance_column + test_finding_incorrect_type_data + end + + def test_update_all_within_inheritance + Client.update_all "name = 'I am a client'" + assert_equal "I am a client", Client.find(:all).first.name + assert_equal "37signals", Firm.find(:all).first.name + end + + def test_alt_update_all_within_inheritance + switch_to_alt_inheritance_column + test_update_all_within_inheritance + end + + def test_destroy_all_within_inheritance + Client.destroy_all + assert_equal 0, Client.count + assert_equal 2, Firm.count + end + + def test_alt_destroy_all_within_inheritance + switch_to_alt_inheritance_column + test_destroy_all_within_inheritance + end + + def test_find_first_within_inheritance + assert_kind_of Firm, Company.find(:first, :conditions => "name = '37signals'") + assert_kind_of Firm, Firm.find(:first, :conditions => "name = '37signals'") + assert_nil Client.find(:first, :conditions => "name = '37signals'") + end + + def test_alt_find_first_within_inheritance + switch_to_alt_inheritance_column + test_find_first_within_inheritance + end + + def test_complex_inheritance + very_special_client = VerySpecialClient.create("name" => "veryspecial") + assert_equal very_special_client, VerySpecialClient.find(:first, :conditions => "name = 'veryspecial'") + assert_equal very_special_client, SpecialClient.find(:first, :conditions => "name = 'veryspecial'") + assert_equal very_special_client, Company.find(:first, :conditions => "name = 'veryspecial'") + assert_equal very_special_client, Client.find(:first, :conditions => "name = 'veryspecial'") + assert_equal 1, Client.find(:all, :conditions => "name = 'Summit'").size + assert_equal very_special_client, Client.find(very_special_client.id) + end + + def test_alt_complex_inheritance + switch_to_alt_inheritance_column + test_complex_inheritance + end + + def test_inheritance_without_mapping + assert_kind_of SpecialSubscriber, SpecialSubscriber.find("webster132") + assert_nothing_raised { s = SpecialSubscriber.new("name" => "And breaaaaathe!"); s.id = 'roger'; s.save } + end + + private + def switch_to_alt_inheritance_column + # we don't want misleading test results, so get rid of the values in the type column + Company.find(:all, :order => 'id').each do |c| + c['type'] = nil + c.save + end + + def Company.inheritance_column() "ruby_type" end + end +end diff --git a/vendor/rails/activerecord/test/lifecycle_test.rb b/vendor/rails/activerecord/test/lifecycle_test.rb new file mode 100755 index 00000000..ddac6f7c --- /dev/null +++ b/vendor/rails/activerecord/test/lifecycle_test.rb @@ -0,0 +1,116 @@ +require 'abstract_unit' +require 'fixtures/topic' +require 'fixtures/developer' +require 'fixtures/reply' + +class Topic; def after_find() end end +class Developer; def after_find() end end +class SpecialDeveloper < Developer; end + +class TopicManualObserver + include Singleton + + attr_reader :action, :object, :callbacks + + def initialize + Topic.add_observer(self) + @callbacks = [] + end + + def update(callback_method, object) + @callbacks << { "callback_method" => callback_method, "object" => object } + end + + def has_been_notified? + !@callbacks.empty? + end +end + +class TopicaObserver < ActiveRecord::Observer + def self.observed_class() Topic end + + attr_reader :topic + + def after_find(topic) + @topic = topic + end +end + +class TopicObserver < ActiveRecord::Observer + attr_reader :topic + + def after_find(topic) + @topic = topic + end +end + +class MultiObserver < ActiveRecord::Observer + attr_reader :record + + def self.observed_class() [ Topic, Developer ] end + + def after_find(record) + @record = record + end + +end + +class LifecycleTest < Test::Unit::TestCase + fixtures :topics, :developers + + def test_before_destroy + assert_equal 2, Topic.count + Topic.find(1).destroy + assert_equal 0, Topic.count + end + + def test_after_save + ActiveRecord::Base.observers = :topic_manual_observer + + topic = Topic.find(1) + topic.title = "hello" + topic.save + + assert TopicManualObserver.instance.has_been_notified? + assert_equal :after_save, TopicManualObserver.instance.callbacks.last["callback_method"] + end + + def test_observer_update_on_save + ActiveRecord::Base.observers = TopicManualObserver + + topic = Topic.find(1) + assert TopicManualObserver.instance.has_been_notified? + assert_equal :after_find, TopicManualObserver.instance.callbacks.first["callback_method"] + end + + def test_auto_observer + topic_observer = TopicaObserver.instance + + topic = Topic.find(1) + assert_equal topic_observer.topic.title, topic.title + end + + def test_infered_auto_observer + topic_observer = TopicObserver.instance + + topic = Topic.find(1) + assert_equal topic_observer.topic.title, topic.title + end + + def test_observing_two_classes + multi_observer = MultiObserver.instance + + topic = Topic.find(1) + assert_equal multi_observer.record.title, topic.title + + developer = Developer.find(1) + assert_equal multi_observer.record.name, developer.name + end + + def test_observing_subclasses + multi_observer = MultiObserver.instance + + developer = SpecialDeveloper.find(1) + assert_equal multi_observer.record.name, developer.name + end +end diff --git a/vendor/rails/activerecord/test/locking_test.rb b/vendor/rails/activerecord/test/locking_test.rb new file mode 100644 index 00000000..105f19f2 --- /dev/null +++ b/vendor/rails/activerecord/test/locking_test.rb @@ -0,0 +1,46 @@ +require 'abstract_unit' +require 'fixtures/person' +require 'fixtures/legacy_thing' + +class LockingTest < Test::Unit::TestCase + fixtures :people, :legacy_things + + def test_lock_existing + p1 = Person.find(1) + p2 = Person.find(1) + + p1.first_name = "Michael" + p1.save + + assert_raises(ActiveRecord::StaleObjectError) { + p2.first_name = "should fail" + p2.save + } + end + + def test_lock_new + p1 = Person.create({ "first_name"=>"anika"}) + p2 = Person.find(p1.id) + assert_equal p1.id, p2.id + p1.first_name = "Anika" + p1.save + + assert_raises(ActiveRecord::StaleObjectError) { + p2.first_name = "should fail" + p2.save + } + end + + def test_lock_column_name_existing + t1 = LegacyThing.find(1) + t2 = LegacyThing.find(1) + t1.tps_report_number = 400 + t1.save + + assert_raises(ActiveRecord::StaleObjectError) { + t2.tps_report_number = 300 + t2.save + } + end + +end diff --git a/vendor/rails/activerecord/test/method_scoping_test.rb b/vendor/rails/activerecord/test/method_scoping_test.rb new file mode 100644 index 00000000..bceb3869 --- /dev/null +++ b/vendor/rails/activerecord/test/method_scoping_test.rb @@ -0,0 +1,416 @@ +require 'abstract_unit' +require 'fixtures/developer' +require 'fixtures/project' +require 'fixtures/comment' +require 'fixtures/post' +require 'fixtures/category' + +class MethodScopingTest < Test::Unit::TestCase + fixtures :developers, :projects, :comments, :posts + + def test_set_conditions + Developer.with_scope(:find => { :conditions => 'just a test...' }) do + assert_equal 'just a test...', Developer.send(:current_scoped_methods)[:find][:conditions] + end + end + + def test_scoped_find + Developer.with_scope(:find => { :conditions => "name = 'David'" }) do + assert_nothing_raised { Developer.find(1) } + end + end + + def test_scoped_find_first + Developer.with_scope(:find => { :conditions => "salary = 100000" }) do + assert_equal Developer.find(10), Developer.find(:first, :order => 'name') + end + end + + def test_scoped_find_combines_conditions + Developer.with_scope(:find => { :conditions => "salary = 9000" }) do + assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => "name = 'Jamis'") + end + end + + def test_scoped_find_sanitizes_conditions + Developer.with_scope(:find => { :conditions => ['salary = ?', 9000] }) do + assert_equal developers(:poor_jamis), Developer.find(:first) + end + end + + def test_scoped_find_combines_and_sanitizes_conditions + Developer.with_scope(:find => { :conditions => ['salary = ?', 9000] }) do + assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => ['name = ?', 'Jamis']) + end + end + + def test_scoped_find_all + Developer.with_scope(:find => { :conditions => "name = 'David'" }) do + assert_equal [developers(:david)], Developer.find(:all) + end + end + + def test_scoped_count + Developer.with_scope(:find => { :conditions => "name = 'David'" }) do + assert_equal 1, Developer.count + end + + Developer.with_scope(:find => { :conditions => 'salary = 100000' }) do + assert_equal 8, Developer.count + assert_equal 1, Developer.count("name LIKE 'fixture_1%'") + end + end + + def test_scoped_find_include + # with the include, will retrieve only developers for the given project + scoped_developers = Developer.with_scope(:find => { :include => :projects }) do + Developer.find(:all, :conditions => 'projects.id = 2') + end + assert scoped_developers.include?(developers(:david)) + assert !scoped_developers.include?(developers(:jamis)) + assert_equal 1, scoped_developers.size + end + + def test_scoped_count_include + # with the include, will retrieve only developers for the given project + Developer.with_scope(:find => { :include => :projects }) do + assert_equal 1, Developer.count('projects.id = 2') + end + end + + def test_scoped_create + new_comment = nil + + VerySpecialComment.with_scope(:create => { :post_id => 1 }) do + assert_equal({ :post_id => 1 }, VerySpecialComment.send(:current_scoped_methods)[:create]) + new_comment = VerySpecialComment.create :body => "Wonderful world" + end + + assert Post.find(1).comments.include?(new_comment) + end + + def test_immutable_scope + options = { :conditions => "name = 'David'" } + Developer.with_scope(:find => options) do + assert_equal %w(David), Developer.find(:all).map { |d| d.name } + options[:conditions] = "name != 'David'" + assert_equal %w(David), Developer.find(:all).map { |d| d.name } + end + + scope = { :find => { :conditions => "name = 'David'" }} + Developer.with_scope(scope) do + assert_equal %w(David), Developer.find(:all).map { |d| d.name } + scope[:find][:conditions] = "name != 'David'" + assert_equal %w(David), Developer.find(:all).map { |d| d.name } + end + end + + def test_scoped_with_duck_typing + scoping = Struct.new(:method_scoping).new(:find => { :conditions => ["name = ?", 'David'] }) + Developer.with_scope(scoping) do + assert_equal %w(David), Developer.find(:all).map { |d| d.name } + end + end + + def test_ensure_that_method_scoping_is_correctly_restored + scoped_methods = Developer.instance_eval('current_scoped_methods') + + begin + Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do + raise "an exception" + end + rescue + end + assert_equal scoped_methods, Developer.instance_eval('current_scoped_methods') + end +end + +class NestedScopingTest < Test::Unit::TestCase + fixtures :developers, :projects, :comments, :posts + + def test_merge_options + Developer.with_scope(:find => { :conditions => 'salary = 80000' }) do + Developer.with_scope(:find => { :limit => 10 }) do + merged_option = Developer.instance_eval('current_scoped_methods')[:find] + assert_equal({ :conditions => 'salary = 80000', :limit => 10 }, merged_option) + end + end + end + + def test_replace_options + Developer.with_scope(:find => { :conditions => "name = 'David'" }) do + Developer.with_exclusive_scope(:find => { :conditions => "name = 'Jamis'" }) do + assert_equal({:find => { :conditions => "name = 'Jamis'" }}, Developer.instance_eval('current_scoped_methods')) + assert_equal({:find => { :conditions => "name = 'Jamis'" }}, Developer.send(:scoped_methods)[-1]) + end + end + end + + def test_append_conditions + Developer.with_scope(:find => { :conditions => "name = 'David'" }) do + Developer.with_scope(:find => { :conditions => 'salary = 80000' }) do + appended_condition = Developer.instance_eval('current_scoped_methods')[:find][:conditions] + assert_equal("( name = 'David' ) AND ( salary = 80000 )", appended_condition) + assert_equal(1, Developer.count) + end + Developer.with_scope(:find => { :conditions => "name = 'Maiha'" }) do + assert_equal(0, Developer.count) + end + end + end + + def test_merge_and_append_options + Developer.with_scope(:find => { :conditions => 'salary = 80000', :limit => 10 }) do + Developer.with_scope(:find => { :conditions => "name = 'David'" }) do + merged_option = Developer.instance_eval('current_scoped_methods')[:find] + assert_equal({ :conditions => "( salary = 80000 ) AND ( name = 'David' )", :limit => 10 }, merged_option) + end + end + end + + def test_nested_scoped_find + Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do + Developer.with_exclusive_scope(:find => { :conditions => "name = 'David'" }) do + assert_nothing_raised { Developer.find(1) } + assert_equal('David', Developer.find(:first).name) + end + assert_equal('Jamis', Developer.find(:first).name) + end + end + + def test_nested_scoped_find_include + Developer.with_scope(:find => { :include => :projects }) do + Developer.with_scope(:find => { :conditions => "projects.id = 2" }) do + assert_nothing_raised { Developer.find(1) } + assert_equal('David', Developer.find(:first).name) + end + end + end + + def test_nested_scoped_find_merged_include + # :include's remain unique and don't "double up" when merging + Developer.with_scope(:find => { :include => :projects, :conditions => "projects.id = 2" }) do + Developer.with_scope(:find => { :include => :projects }) do + assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:include].length + assert_equal('David', Developer.find(:first).name) + end + end + + # the nested scope doesn't remove the first :include + Developer.with_scope(:find => { :include => :projects, :conditions => "projects.id = 2" }) do + Developer.with_scope(:find => { :include => [] }) do + assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:include].length + assert_equal('David', Developer.find(:first).name) + end + end + + # mixing array and symbol include's will merge correctly + Developer.with_scope(:find => { :include => [:projects], :conditions => "projects.id = 2" }) do + Developer.with_scope(:find => { :include => :projects }) do + assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:include].length + assert_equal('David', Developer.find(:first).name) + end + end + end + + def test_nested_scoped_find_replace_include + Developer.with_scope(:find => { :include => :projects }) do + Developer.with_exclusive_scope(:find => { :include => [] }) do + assert_equal 0, Developer.instance_eval('current_scoped_methods')[:find][:include].length + end + end + end + + def test_three_level_nested_exclusive_scoped_find + Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do + assert_equal('Jamis', Developer.find(:first).name) + + Developer.with_exclusive_scope(:find => { :conditions => "name = 'David'" }) do + assert_equal('David', Developer.find(:first).name) + + Developer.with_exclusive_scope(:find => { :conditions => "name = 'Maiha'" }) do + assert_equal(nil, Developer.find(:first)) + end + + # ensure that scoping is restored + assert_equal('David', Developer.find(:first).name) + end + + # ensure that scoping is restored + assert_equal('Jamis', Developer.find(:first).name) + end + end + + def test_merged_scoped_find + poor_jamis = developers(:poor_jamis) + Developer.with_scope(:find => { :conditions => "salary < 100000" }) do + Developer.with_scope(:find => { :offset => 1 }) do + assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc')) + end + end + end + + def test_merged_scoped_find_sanitizes_conditions + Developer.with_scope(:find => { :conditions => ["name = ?", 'David'] }) do + Developer.with_scope(:find => { :conditions => ['salary = ?', 9000] }) do + assert_raise(ActiveRecord::RecordNotFound) { developers(:poor_jamis) } + end + end + end + + def test_nested_scoped_find_combines_and_sanitizes_conditions + Developer.with_scope(:find => { :conditions => ["name = ?", 'David'] }) do + Developer.with_exclusive_scope(:find => { :conditions => ['salary = ?', 9000] }) do + assert_equal developers(:poor_jamis), Developer.find(:first) + assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => ['name = ?', 'Jamis']) + end + end + end + + def test_merged_scoped_find_combines_and_sanitizes_conditions + Developer.with_scope(:find => { :conditions => ["name = ?", 'David'] }) do + Developer.with_scope(:find => { :conditions => ['salary > ?', 9000] }) do + assert_equal %w(David), Developer.find(:all).map { |d| d.name } + end + end + end + + def test_immutable_nested_scope + options1 = { :conditions => "name = 'Jamis'" } + options2 = { :conditions => "name = 'David'" } + Developer.with_scope(:find => options1) do + Developer.with_exclusive_scope(:find => options2) do + assert_equal %w(David), Developer.find(:all).map { |d| d.name } + options1[:conditions] = options2[:conditions] = nil + assert_equal %w(David), Developer.find(:all).map { |d| d.name } + end + end + end + + def test_immutable_merged_scope + options1 = { :conditions => "name = 'Jamis'" } + options2 = { :conditions => "salary > 10000" } + Developer.with_scope(:find => options1) do + Developer.with_scope(:find => options2) do + assert_equal %w(Jamis), Developer.find(:all).map { |d| d.name } + options1[:conditions] = options2[:conditions] = nil + assert_equal %w(Jamis), Developer.find(:all).map { |d| d.name } + end + end + end + + def test_ensure_that_method_scoping_is_correctly_restored + Developer.with_scope(:find => { :conditions => "name = 'David'" }) do + scoped_methods = Developer.instance_eval('current_scoped_methods') + begin + Developer.with_scope(:find => { :conditions => "name = 'Maiha'" }) do + raise "an exception" + end + rescue + end + assert_equal scoped_methods, Developer.instance_eval('current_scoped_methods') + end + end +end + +class HasManyScopingTest< Test::Unit::TestCase + fixtures :comments, :posts + + def setup + @welcome = Post.find(1) + end + + def test_forwarding_of_static_methods + assert_equal 'a comment...', Comment.what_are_you + assert_equal 'a comment...', @welcome.comments.what_are_you + end + + def test_forwarding_to_scoped + assert_equal 4, Comment.search_by_type('Comment').size + assert_equal 2, @welcome.comments.search_by_type('Comment').size + end + + def test_forwarding_to_dynamic_finders + assert_equal 4, Comment.find_all_by_type('Comment').size + assert_equal 2, @welcome.comments.find_all_by_type('Comment').size + end + + def test_nested_scope + Comment.with_scope(:find => { :conditions => '1=1' }) do + assert_equal 'a comment...', @welcome.comments.what_are_you + end + end +end + + +class HasAndBelongsToManyScopingTest< Test::Unit::TestCase + fixtures :posts, :categories, :categories_posts + + def setup + @welcome = Post.find(1) + end + + def test_forwarding_of_static_methods + assert_equal 'a category...', Category.what_are_you + assert_equal 'a category...', @welcome.categories.what_are_you + end + + def test_forwarding_to_dynamic_finders + assert_equal 4, Category.find_all_by_type('SpecialCategory').size + assert_equal 0, @welcome.categories.find_all_by_type('SpecialCategory').size + assert_equal 2, @welcome.categories.find_all_by_type('Category').size + end + + def test_nested_scope + Category.with_scope(:find => { :conditions => '1=1' }) do + assert_equal 'a comment...', @welcome.comments.what_are_you + end + end +end + + +=begin +# We disabled the scoping for has_one and belongs_to as we can't think of a proper use case + + +class BelongsToScopingTest< Test::Unit::TestCase + fixtures :comments, :posts + + def setup + @greetings = Comment.find(1) + end + + def test_forwarding_of_static_method + assert_equal 'a post...', Post.what_are_you + assert_equal 'a post...', @greetings.post.what_are_you + end + + def test_forwarding_to_dynamic_finders + assert_equal 4, Post.find_all_by_type('Post').size + assert_equal 1, @greetings.post.find_all_by_type('Post').size + end + +end + + +class HasOneScopingTest< Test::Unit::TestCase + fixtures :comments, :posts + + def setup + @sti_comments = Post.find(4) + end + + def test_forwarding_of_static_methods + assert_equal 'a comment...', Comment.what_are_you + assert_equal 'a very special comment...', @sti_comments.very_special_comment.what_are_you + end + + def test_forwarding_to_dynamic_finders + assert_equal 1, Comment.find_all_by_type('VerySpecialComment').size + assert_equal 1, @sti_comments.very_special_comment.find_all_by_type('VerySpecialComment').size + assert_equal 0, @sti_comments.very_special_comment.find_all_by_type('Comment').size + end + +end + +=end diff --git a/vendor/rails/activerecord/test/migration_test.rb b/vendor/rails/activerecord/test/migration_test.rb new file mode 100644 index 00000000..c6a1d924 --- /dev/null +++ b/vendor/rails/activerecord/test/migration_test.rb @@ -0,0 +1,491 @@ +require 'abstract_unit' +require 'fixtures/person' +require File.dirname(__FILE__) + '/fixtures/migrations/1_people_have_last_names' +require File.dirname(__FILE__) + '/fixtures/migrations/2_we_need_reminders' + +if ActiveRecord::Base.connection.supports_migrations? + class Reminder < ActiveRecord::Base; end + + class ActiveRecord::Migration + class < "key", :unique => true) } + assert_nothing_raised { Person.connection.remove_index("people", :name => "key") } + + # Sybase adapter does not support indexes on :boolean columns + unless current_adapter?(:SybaseAdapter) + assert_nothing_raised { Person.connection.add_index("people", %w(last_name first_name administrator), :name => "named_admin") } + assert_nothing_raised { Person.connection.remove_index("people", :name => "named_admin") } + end + end + + def test_create_table_adds_id + Person.connection.create_table :testings do |t| + t.column :foo, :string + end + + assert_equal %w(foo id), + Person.connection.columns(:testings).map { |c| c.name }.sort + ensure + Person.connection.drop_table :testings rescue nil + end + + def test_create_table_with_not_null_column + Person.connection.create_table :testings do |t| + t.column :foo, :string, :null => false + end + + assert_raises(ActiveRecord::StatementInvalid) do + Person.connection.execute "insert into testings (foo) values (NULL)" + end + ensure + Person.connection.drop_table :testings rescue nil + end + + def test_create_table_with_defaults + Person.connection.create_table :testings do |t| + t.column :one, :string, :default => "hello" + t.column :two, :boolean, :default => true + t.column :three, :boolean, :default => false + t.column :four, :integer, :default => 1 + end + + columns = Person.connection.columns(:testings) + one = columns.detect { |c| c.name == "one" } + two = columns.detect { |c| c.name == "two" } + three = columns.detect { |c| c.name == "three" } + four = columns.detect { |c| c.name == "four" } + + assert_equal "hello", one.default + if current_adapter?(:OracleAdapter) + # Oracle doesn't support native booleans + assert_equal true, two.default == 1 + assert_equal false, three.default != 0 + else + assert_equal true, two.default + assert_equal false, three.default + end + assert_equal 1, four.default + + ensure + Person.connection.drop_table :testings rescue nil + end + + # SQL Server and Sybase will not allow you to add a NOT NULL column + # to a table without specifying a default value, so the + # following test must be skipped + unless current_adapter?(:SQLServerAdapter) || current_adapter?(:SybaseAdapter) + def test_add_column_not_null_without_default + Person.connection.create_table :testings do |t| + t.column :foo, :string + end + Person.connection.add_column :testings, :bar, :string, :null => false + + assert_raises(ActiveRecord::StatementInvalid) do + Person.connection.execute "insert into testings (foo, bar) values ('hello', NULL)" + end + ensure + Person.connection.drop_table :testings rescue nil + end + end + + def test_add_column_not_null_with_default + Person.connection.create_table :testings do |t| + t.column :foo, :string + end + Person.connection.add_column :testings, :bar, :string, :null => false, :default => "default" + + assert_raises(ActiveRecord::StatementInvalid) do + Person.connection.execute "insert into testings (foo, bar) values ('hello', NULL)" + end + ensure + Person.connection.drop_table :testings rescue nil + end + + def test_native_types + Person.delete_all + Person.connection.add_column "people", "last_name", :string + Person.connection.add_column "people", "bio", :text + Person.connection.add_column "people", "age", :integer + Person.connection.add_column "people", "height", :float + Person.connection.add_column "people", "birthday", :datetime + Person.connection.add_column "people", "favorite_day", :date + Person.connection.add_column "people", "male", :boolean + assert_nothing_raised { Person.create :first_name => 'bob', :last_name => 'bobsen', :bio => "I was born ....", :age => 18, :height => 1.78, :birthday => 18.years.ago, :favorite_day => 10.days.ago, :male => true } + bob = Person.find(:first) + + assert_equal bob.first_name, 'bob' + assert_equal bob.last_name, 'bobsen' + assert_equal bob.bio, "I was born ...." + assert_equal bob.age, 18 + assert_equal bob.male?, true + + assert_equal String, bob.first_name.class + assert_equal String, bob.last_name.class + assert_equal String, bob.bio.class + assert_equal Fixnum, bob.age.class + assert_equal Time, bob.birthday.class + + if current_adapter?(:SQLServerAdapter) || current_adapter?(:OracleAdapter) || current_adapter?(:SybaseAdapter) + # SQL Server, Sybase, and Oracle don't differentiate between date/time + assert_equal Time, bob.favorite_day.class + else + assert_equal Date, bob.favorite_day.class + end + + assert_equal TrueClass, bob.male?.class + end + + def test_add_remove_single_field_using_string_arguments + assert !Person.column_methods_hash.include?(:last_name) + + ActiveRecord::Migration.add_column 'people', 'last_name', :string + + Person.reset_column_information + assert Person.column_methods_hash.include?(:last_name) + + ActiveRecord::Migration.remove_column 'people', 'last_name' + + Person.reset_column_information + assert !Person.column_methods_hash.include?(:last_name) + end + + def test_add_remove_single_field_using_symbol_arguments + assert !Person.column_methods_hash.include?(:last_name) + + ActiveRecord::Migration.add_column :people, :last_name, :string + + Person.reset_column_information + assert Person.column_methods_hash.include?(:last_name) + + ActiveRecord::Migration.remove_column :people, :last_name + + Person.reset_column_information + assert !Person.column_methods_hash.include?(:last_name) + end + + def test_add_rename + Person.delete_all + + begin + Person.connection.add_column "people", "girlfriend", :string + Person.create :girlfriend => 'bobette' + + Person.connection.rename_column "people", "girlfriend", "exgirlfriend" + + Person.reset_column_information + bob = Person.find(:first) + + assert_equal "bobette", bob.exgirlfriend + ensure + Person.connection.remove_column("people", "girlfriend") rescue nil + Person.connection.remove_column("people", "exgirlfriend") rescue nil + end + + end + + def test_rename_column_using_symbol_arguments + begin + Person.connection.rename_column :people, :first_name, :nick_name + Person.reset_column_information + assert Person.column_names.include?("nick_name") + ensure + Person.connection.remove_column("people","nick_name") + Person.connection.add_column("people","first_name", :string) + end + end + + def test_rename_column + begin + Person.connection.rename_column "people", "first_name", "nick_name" + Person.reset_column_information + assert Person.column_names.include?("nick_name") + ensure + Person.connection.remove_column("people","nick_name") + Person.connection.add_column("people","first_name", :string) + end + end + + def test_rename_table + begin + ActiveRecord::Base.connection.create_table :octopuses do |t| + t.column :url, :string + end + ActiveRecord::Base.connection.rename_table :octopuses, :octopi + + assert_nothing_raised do + if current_adapter?(:OracleAdapter) + # Oracle requires the explicit sequence value for the pk + ActiveRecord::Base.connection.execute "INSERT INTO octopi (id, url) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" + else + ActiveRecord::Base.connection.execute "INSERT INTO octopi (url) VALUES ('http://www.foreverflying.com/octopus-black7.jpg')" + end + end + + assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', ActiveRecord::Base.connection.select_value("SELECT url FROM octopi WHERE id=1") + + ensure + ActiveRecord::Base.connection.drop_table :octopuses rescue nil + ActiveRecord::Base.connection.drop_table :octopi rescue nil + end + end + + def test_change_column + Person.connection.add_column 'people', 'age', :integer + old_columns = Person.connection.columns(Person.table_name, "#{name} Columns") + assert old_columns.find { |c| c.name == 'age' and c.type == :integer } + + assert_nothing_raised { Person.connection.change_column "people", "age", :string } + + new_columns = Person.connection.columns(Person.table_name, "#{name} Columns") + assert_nil new_columns.find { |c| c.name == 'age' and c.type == :integer } + assert new_columns.find { |c| c.name == 'age' and c.type == :string } + end + + def test_change_column_with_new_default + Person.connection.add_column "people", "administrator", :boolean, :default => 1 + Person.reset_column_information + assert Person.new.administrator? + + assert_nothing_raised { Person.connection.change_column "people", "administrator", :boolean, :default => 0 } + Person.reset_column_information + assert !Person.new.administrator? + end + + def test_add_table + assert !Reminder.table_exists? + + WeNeedReminders.up + + assert Reminder.create("content" => "hello world", "remind_at" => Time.now) + assert_equal "hello world", Reminder.find(:first).content + + WeNeedReminders.down + assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + end + + def test_migrator + assert !Person.column_methods_hash.include?(:last_name) + assert !Reminder.table_exists? + + ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/') + + assert_equal 3, ActiveRecord::Migrator.current_version + Person.reset_column_information + assert Person.column_methods_hash.include?(:last_name) + assert Reminder.create("content" => "hello world", "remind_at" => Time.now) + assert_equal "hello world", Reminder.find(:first).content + + ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/') + + assert_equal 0, ActiveRecord::Migrator.current_version + Person.reset_column_information + assert !Person.column_methods_hash.include?(:last_name) + assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + end + + def test_migrator_one_up + assert !Person.column_methods_hash.include?(:last_name) + assert !Reminder.table_exists? + + ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 1) + + Person.reset_column_information + assert Person.column_methods_hash.include?(:last_name) + assert !Reminder.table_exists? + + ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 2) + + assert Reminder.create("content" => "hello world", "remind_at" => Time.now) + assert_equal "hello world", Reminder.find(:first).content + end + + def test_migrator_one_down + ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/') + + ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/', 1) + + Person.reset_column_information + assert Person.column_methods_hash.include?(:last_name) + assert !Reminder.table_exists? + end + + def test_migrator_one_up_one_down + ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 1) + ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/', 0) + + assert !Person.column_methods_hash.include?(:last_name) + assert !Reminder.table_exists? + end + + def test_migrator_verbosity + ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 1) + assert PeopleHaveLastNames.message_count > 0 + PeopleHaveLastNames.message_count = 0 + + ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/', 0) + assert PeopleHaveLastNames.message_count > 0 + PeopleHaveLastNames.message_count = 0 + end + + def test_migrator_verbosity_off + PeopleHaveLastNames.verbose = false + ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 1) + assert PeopleHaveLastNames.message_count.zero? + ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/', 0) + assert PeopleHaveLastNames.message_count.zero? + end + + def test_migrator_going_down_due_to_version_target + ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 1) + ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + '/fixtures/migrations/', 0) + + assert !Person.column_methods_hash.include?(:last_name) + assert !Reminder.table_exists? + + ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + '/fixtures/migrations/') + + Person.reset_column_information + assert Person.column_methods_hash.include?(:last_name) + assert Reminder.create("content" => "hello world", "remind_at" => Time.now) + assert_equal "hello world", Reminder.find(:first).content + end + + def test_schema_info_table_name + ActiveRecord::Base.table_name_prefix = "prefix_" + ActiveRecord::Base.table_name_suffix = "_suffix" + Reminder.reset_table_name + assert_equal "prefix_schema_info_suffix", ActiveRecord::Migrator.schema_info_table_name + ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::Base.table_name_suffix = "" + Reminder.reset_table_name + assert_equal "schema_info", ActiveRecord::Migrator.schema_info_table_name + ensure + ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::Base.table_name_suffix = "" + end + + def test_proper_table_name + assert_equal "table", ActiveRecord::Migrator.proper_table_name('table') + assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table) + assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder) + Reminder.reset_table_name + assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder) + + # Use the model's own prefix/suffix if a model is given + ActiveRecord::Base.table_name_prefix = "ARprefix_" + ActiveRecord::Base.table_name_suffix = "_ARsuffix" + Reminder.table_name_prefix = 'prefix_' + Reminder.table_name_suffix = '_suffix' + Reminder.reset_table_name + assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder) + Reminder.table_name_prefix = '' + Reminder.table_name_suffix = '' + Reminder.reset_table_name + + # Use AR::Base's prefix/suffix if string or symbol is given + ActiveRecord::Base.table_name_prefix = "prefix_" + ActiveRecord::Base.table_name_suffix = "_suffix" + Reminder.reset_table_name + assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table') + assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table) + ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::Base.table_name_suffix = "" + Reminder.reset_table_name + end + + def test_add_drop_table_with_prefix_and_suffix + assert !Reminder.table_exists? + ActiveRecord::Base.table_name_prefix = 'prefix_' + ActiveRecord::Base.table_name_suffix = '_suffix' + Reminder.reset_table_name + Reminder.reset_sequence_name + WeNeedReminders.up + assert Reminder.create("content" => "hello world", "remind_at" => Time.now) + assert_equal "hello world", Reminder.find(:first).content + + WeNeedReminders.down + assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + ensure + ActiveRecord::Base.table_name_prefix = '' + ActiveRecord::Base.table_name_suffix = '' + Reminder.reset_table_name + Reminder.reset_sequence_name + end + + def test_create_table_with_binary_column + Person.connection.drop_table :binary_testings rescue nil + + assert_nothing_raised { + Person.connection.create_table :binary_testings do |t| + t.column "data", :binary, :default => "", :null => false + end + } + + columns = Person.connection.columns(:binary_testings) + data_column = columns.detect { |c| c.name == "data" } + + if current_adapter?(:OracleAdapter) + assert_equal "empty_blob()", data_column.default + else + assert_equal "", data_column.default + end + + Person.connection.drop_table :binary_testings rescue nil + end + + def test_migrator_with_duplicates + assert_raises(ActiveRecord::DuplicateMigrationVersionError) do + ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + '/fixtures/migrations_with_duplicate/', nil) + end + end + end +end diff --git a/vendor/rails/activerecord/test/mixin_nested_set_test.rb b/vendor/rails/activerecord/test/mixin_nested_set_test.rb new file mode 100644 index 00000000..8764a6ac --- /dev/null +++ b/vendor/rails/activerecord/test/mixin_nested_set_test.rb @@ -0,0 +1,184 @@ +require 'abstract_unit' +require 'active_record/acts/nested_set' +require 'fixtures/mixin' +require 'pp' + +class MixinNestedSetTest < Test::Unit::TestCase + fixtures :mixins + + def test_mixing_in_methods + ns = NestedSet.new + assert( ns.respond_to?( :all_children ) ) + assert_equal( ns.scope_condition, "root_id IS NULL" ) + + check_method_mixins ns + end + + def test_string_scope + ns = NestedSetWithStringScope.new + + ns.root_id = 1 + assert_equal( ns.scope_condition, "root_id = 1" ) + ns.root_id = 42 + assert_equal( ns.scope_condition, "root_id = 42" ) + check_method_mixins ns + end + + def test_symbol_scope + ns = NestedSetWithSymbolScope.new + ns.root_id = 1 + assert_equal( ns.scope_condition, "root_id = 1" ) + ns.root_id = 42 + assert_equal( ns.scope_condition, "root_id = 42" ) + check_method_mixins ns + end + + def check_method_mixins( obj ) + [:scope_condition, :left_col_name, :right_col_name, :parent_column, :root?, :add_child, + :children_count, :full_set, :all_children, :direct_children].each { |symbol| assert( obj.respond_to?(symbol)) } + end + + def set( id ) + NestedSet.find( 3000 + id ) + end + + def test_adding_children + assert( set(1).unknown? ) + assert( set(2).unknown? ) + set(1).add_child set(2) + + # Did we maintain adding the parent_ids? + assert( set(1).root? ) + assert( set(2).child? ) + assert( set(2).parent_id == set(1).id ) + + # Check boundies + assert_equal( set(1).lft, 1 ) + assert_equal( set(2).lft, 2 ) + assert_equal( set(2).rgt, 3 ) + assert_equal( set(1).rgt, 4 ) + + # Check children cound + assert_equal( set(1).children_count, 1 ) + + set(1).add_child set(3) + + #check boundries + assert_equal( set(1).lft, 1 ) + assert_equal( set(2).lft, 2 ) + assert_equal( set(2).rgt, 3 ) + assert_equal( set(3).lft, 4 ) + assert_equal( set(3).rgt, 5 ) + assert_equal( set(1).rgt, 6 ) + + # How is the count looking? + assert_equal( set(1).children_count, 2 ) + + set(2).add_child set(4) + + # boundries + assert_equal( set(1).lft, 1 ) + assert_equal( set(2).lft, 2 ) + assert_equal( set(4).lft, 3 ) + assert_equal( set(4).rgt, 4 ) + assert_equal( set(2).rgt, 5 ) + assert_equal( set(3).lft, 6 ) + assert_equal( set(3).rgt, 7 ) + assert_equal( set(1).rgt, 8 ) + + # Children count + assert_equal( set(1).children_count, 3 ) + assert_equal( set(2).children_count, 1 ) + assert_equal( set(3).children_count, 0 ) + assert_equal( set(4).children_count, 0 ) + + set(2).add_child set(5) + set(4).add_child set(6) + + assert_equal( set(2).children_count, 3 ) + + + # Children accessors + assert_equal( set(1).full_set.length, 6 ) + assert_equal( set(2).full_set.length, 4 ) + assert_equal( set(4).full_set.length, 2 ) + + assert_equal( set(1).all_children.length, 5 ) + assert_equal( set(6).all_children.length, 0 ) + + assert_equal( set(1).direct_children.length, 2 ) + + end + + def test_snipping_tree + big_tree = NestedSetWithStringScope.find( 4001 ) + + # Make sure we have the right one + assert_equal( 3, big_tree.direct_children.length ) + assert_equal( 10, big_tree.full_set.length ) + + NestedSetWithStringScope.find( 4005 ).destroy + + big_tree = NestedSetWithStringScope.find( 4001 ) + + assert_equal( 7, big_tree.full_set.length ) + assert_equal( 2, big_tree.direct_children.length ) + + assert_equal( 1, NestedSetWithStringScope.find(4001).lft ) + assert_equal( 2, NestedSetWithStringScope.find(4002).lft ) + assert_equal( 3, NestedSetWithStringScope.find(4003).lft ) + assert_equal( 4, NestedSetWithStringScope.find(4003).rgt ) + assert_equal( 5, NestedSetWithStringScope.find(4004).lft ) + assert_equal( 6, NestedSetWithStringScope.find(4004).rgt ) + assert_equal( 7, NestedSetWithStringScope.find(4002).rgt ) + assert_equal( 8, NestedSetWithStringScope.find(4008).lft ) + assert_equal( 9, NestedSetWithStringScope.find(4009).lft ) + assert_equal(10, NestedSetWithStringScope.find(4009).rgt ) + assert_equal(11, NestedSetWithStringScope.find(4010).lft ) + assert_equal(12, NestedSetWithStringScope.find(4010).rgt ) + assert_equal(13, NestedSetWithStringScope.find(4008).rgt ) + assert_equal(14, NestedSetWithStringScope.find(4001).rgt ) + end + + def test_deleting_root + NestedSetWithStringScope.find(4001).destroy + + assert( NestedSetWithStringScope.count == 0 ) + end + + def test_common_usage + mixins(:set_1).add_child( mixins(:set_2) ) + assert_equal( 1, mixins(:set_1).direct_children.length ) + + mixins(:set_2).add_child( mixins(:set_3) ) + assert_equal( 1, mixins(:set_1).direct_children.length ) + + # Local cache is now out of date! + # Problem: the update_alls update all objects up the tree + mixins(:set_1).reload + assert_equal( 2, mixins(:set_1).all_children.length ) + + assert_equal( 1, mixins(:set_1).lft ) + assert_equal( 2, mixins(:set_2).lft ) + assert_equal( 3, mixins(:set_3).lft ) + assert_equal( 4, mixins(:set_3).rgt ) + assert_equal( 5, mixins(:set_2).rgt ) + assert_equal( 6, mixins(:set_1).rgt ) + + assert( mixins(:set_1).root? ) + + begin + mixins(:set_4).add_child( mixins(:set_1) ) + fail + rescue + end + + assert_equal( 2, mixins(:set_1).all_children.length ) + + mixins(:set_1).add_child mixins(:set_4) + + assert_equal( 3, mixins(:set_1).all_children.length ) + + + end +end diff --git a/vendor/rails/activerecord/test/mixin_test.rb b/vendor/rails/activerecord/test/mixin_test.rb new file mode 100644 index 00000000..8ab73cfe --- /dev/null +++ b/vendor/rails/activerecord/test/mixin_test.rb @@ -0,0 +1,512 @@ +require 'abstract_unit' +require 'active_record/acts/tree' +require 'active_record/acts/list' +require 'active_record/acts/nested_set' +require 'fixtures/mixin' + +class ListTest < Test::Unit::TestCase + fixtures :mixins + + def test_reordering + 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).move_lower + + assert_equal [mixins(:list_1), + mixins(:list_3), + mixins(:list_2), + mixins(:list_4)], + ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos') + + mixins(:list_2).move_higher + + 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_1).move_to_bottom + + assert_equal [mixins(:list_2), + mixins(:list_3), + mixins(:list_4), + mixins(:list_1)], + ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos') + + mixins(:list_1).move_to_top + + 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).move_to_bottom + + assert_equal [mixins(:list_1), + mixins(:list_3), + mixins(:list_4), + mixins(:list_2)], + ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos') + + mixins(:list_4).move_to_top + + assert_equal [mixins(:list_4), + mixins(:list_1), + mixins(:list_3), + mixins(:list_2)], + ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos') + + end + + def test_move_to_bottom_with_next_to_last_item + 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_3).move_to_bottom + + assert_equal [mixins(:list_1), + mixins(:list_2), + mixins(:list_4), + mixins(:list_3)], + ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos') + end + + def test_next_prev + assert_equal mixins(:list_2), mixins(:list_1).lower_item + assert_nil mixins(:list_1).higher_item + assert_equal mixins(:list_3), mixins(:list_4).higher_item + assert_nil mixins(:list_4).lower_item + end + + + def test_injection + item = ListMixin.new("parent_id"=>1) + assert_equal "parent_id = 1", item.scope_condition + assert_equal "pos", item.position_column + end + + def test_insert + new = ListMixin.create("parent_id"=>20) + assert_equal 1, new.pos + assert new.first? + assert new.last? + + new = ListMixin.create("parent_id"=>20) + assert_equal 2, new.pos + assert !new.first? + assert new.last? + + new = ListMixin.create("parent_id"=>20) + assert_equal 3, new.pos + assert !new.first? + assert new.last? + + new = ListMixin.create("parent_id"=>0) + assert_equal 1, new.pos + assert new.first? + assert new.last? + end + + def test_insert_at + new = ListMixin.create("parent_id" => 20) + assert_equal 1, new.pos + + new = ListMixin.create("parent_id" => 20) + assert_equal 2, new.pos + + new = ListMixin.create("parent_id" => 20) + assert_equal 3, new.pos + + new4 = ListMixin.create("parent_id" => 20) + assert_equal 4, new4.pos + + new4.insert_at(3) + assert_equal 3, new4.pos + + new.reload + assert_equal 4, new.pos + + new.insert_at(2) + assert_equal 2, new.pos + + new4.reload + assert_equal 4, new4.pos + + new5 = ListMixin.create("parent_id" => 20) + assert_equal 5, new5.pos + + new5.insert_at(1) + assert_equal 1, new5.pos + + new4.reload + assert_equal 5, new4.pos + end + + def test_delete_middle + 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).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 + + mixins(:list_1).destroy + + assert_equal [mixins(:list_3, :reload), + mixins(:list_4, :reload)], + ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos') + + assert_equal 1, mixins(:list_3).pos + assert_equal 2, mixins(:list_4).pos + + end + + def test_with_string_based_scope + new = ListWithStringScopeMixin.create("parent_id"=>500) + assert_equal 1, new.pos + assert new.first? + assert new.last? + end + + def test_nil_scope + new1, new2, new3 = ListMixin.create, ListMixin.create, ListMixin.create + new2.move_higher + assert_equal [new2, new1, new3], ListMixin.find(:all, :conditions => 'parent_id IS NULL', :order => 'pos') + end + +end + +class TreeTest < Test::Unit::TestCase + fixtures :mixins + + def test_has_child + assert_equal true, mixins(:tree_1).has_children? + assert_equal true, mixins(:tree_2).has_children? + assert_equal false, mixins(:tree_3).has_children? + assert_equal false, mixins(:tree_4).has_children? + end + + def test_children + assert_equal mixins(:tree_1).children, [mixins(:tree_2), mixins(:tree_4)] + assert_equal mixins(:tree_2).children, [mixins(:tree_3)] + assert_equal mixins(:tree_3).children, [] + assert_equal mixins(:tree_4).children, [] + end + + def test_has_parent + assert_equal false, mixins(:tree_1).has_parent? + assert_equal true, mixins(:tree_2).has_parent? + assert_equal true, mixins(:tree_3).has_parent? + assert_equal true, mixins(:tree_4).has_parent? + end + + def test_parent + assert_equal mixins(:tree_2).parent, mixins(:tree_1) + assert_equal mixins(:tree_2).parent, mixins(:tree_4).parent + assert_nil mixins(:tree_1).parent + end + + def test_delete + assert_equal 6, TreeMixin.count + mixins(:tree_1).destroy + assert_equal 2, TreeMixin.count + mixins(:tree2_1).destroy + mixins(:tree3_1).destroy + assert_equal 0, TreeMixin.count + end + + def test_insert + @extra = mixins(:tree_1).children.create + + assert @extra + + assert_equal @extra.parent, mixins(:tree_1) + + assert_equal 3, mixins(:tree_1).children.size + assert mixins(:tree_1).children.include?(@extra) + assert mixins(:tree_1).children.include?(mixins(:tree_2)) + assert mixins(:tree_1).children.include?(mixins(:tree_4)) + end + + def test_ancestors + assert_equal [], mixins(:tree_1).ancestors + assert_equal [mixins(:tree_1)], mixins(:tree_2).ancestors + assert_equal [mixins(:tree_2), mixins(:tree_1)], mixins(:tree_3).ancestors + assert_equal [mixins(:tree_1)], mixins(:tree_4).ancestors + assert_equal [], mixins(:tree2_1).ancestors + assert_equal [], mixins(:tree3_1).ancestors + end + + def test_root + assert_equal mixins(:tree_1), TreeMixin.root + assert_equal mixins(:tree_1), mixins(:tree_1).root + assert_equal mixins(:tree_1), mixins(:tree_2).root + assert_equal mixins(:tree_1), mixins(:tree_3).root + assert_equal mixins(:tree_1), mixins(:tree_4).root + assert_equal mixins(:tree2_1), mixins(:tree2_1).root + assert_equal mixins(:tree3_1), mixins(:tree3_1).root + end + + def test_roots + assert_equal [mixins(:tree_1), mixins(:tree2_1), mixins(:tree3_1)], TreeMixin.roots + end + + def test_siblings + assert_equal [mixins(:tree2_1), mixins(:tree3_1)], mixins(:tree_1).siblings + assert_equal [mixins(:tree_4)], mixins(:tree_2).siblings + assert_equal [], mixins(:tree_3).siblings + assert_equal [mixins(:tree_2)], mixins(:tree_4).siblings + assert_equal [mixins(:tree_1), mixins(:tree3_1)], mixins(:tree2_1).siblings + assert_equal [mixins(:tree_1), mixins(:tree2_1)], mixins(:tree3_1).siblings + end + + def test_self_and_siblings + assert_equal [mixins(:tree_1), mixins(:tree2_1), mixins(:tree3_1)], mixins(:tree_1).self_and_siblings + assert_equal [mixins(:tree_2), mixins(:tree_4)], mixins(:tree_2).self_and_siblings + assert_equal [mixins(:tree_3)], mixins(:tree_3).self_and_siblings + assert_equal [mixins(:tree_2), mixins(:tree_4)], mixins(:tree_4).self_and_siblings + assert_equal [mixins(:tree_1), mixins(:tree2_1), mixins(:tree3_1)], mixins(:tree2_1).self_and_siblings + assert_equal [mixins(:tree_1), mixins(:tree2_1), mixins(:tree3_1)], mixins(:tree3_1).self_and_siblings + end +end + +class TreeTestWithoutOrder < Test::Unit::TestCase + fixtures :mixins + + def test_root + assert [mixins(:tree_without_order_1), mixins(:tree_without_order_2)].include?(TreeMixinWithoutOrder.root) + end + + def test_roots + assert_equal [], [mixins(:tree_without_order_1), mixins(:tree_without_order_2)] - TreeMixinWithoutOrder.roots + end +end + +class TouchTest < Test::Unit::TestCase + fixtures :mixins + + def test_update + stamped = Mixin.new + + assert_nil stamped.updated_at + assert_nil stamped.created_at + stamped.save + assert_not_nil stamped.updated_at + assert_not_nil stamped.created_at + end + + def test_create + @obj = Mixin.create + assert_not_nil @obj.updated_at + assert_not_nil @obj.created_at + end + + def test_many_updates + stamped = Mixin.new + + assert_nil stamped.updated_at + assert_nil stamped.created_at + stamped.save + assert_not_nil stamped.created_at + assert_not_nil stamped.updated_at + + old_updated_at = stamped.updated_at + + sleep 1 + stamped.save + assert_not_equal stamped.created_at, stamped.updated_at + assert_not_equal old_updated_at, stamped.updated_at + + end + + def test_create_turned_off + Mixin.record_timestamps = false + + assert_nil mixins(:tree_1).updated_at + mixins(:tree_1).save + assert_nil mixins(:tree_1).updated_at + + Mixin.record_timestamps = true + end + +end + + +class ListSubTest < Test::Unit::TestCase + fixtures :mixins + + def test_reordering + assert_equal [mixins(:list_sub_1), + mixins(:list_sub_2), + mixins(:list_sub_3), + mixins(:list_sub_4)], + ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos') + + mixins(:list_sub_2).move_lower + + assert_equal [mixins(:list_sub_1), + mixins(:list_sub_3), + mixins(:list_sub_2), + mixins(:list_sub_4)], + ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos') + + mixins(:list_sub_2).move_higher + + assert_equal [mixins(:list_sub_1), + mixins(:list_sub_2), + mixins(:list_sub_3), + mixins(:list_sub_4)], + ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos') + + mixins(:list_sub_1).move_to_bottom + + assert_equal [mixins(:list_sub_2), + mixins(:list_sub_3), + mixins(:list_sub_4), + mixins(:list_sub_1)], + ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos') + + mixins(:list_sub_1).move_to_top + + assert_equal [mixins(:list_sub_1), + mixins(:list_sub_2), + mixins(:list_sub_3), + mixins(:list_sub_4)], + ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos') + + + mixins(:list_sub_2).move_to_bottom + + assert_equal [mixins(:list_sub_1), + mixins(:list_sub_3), + mixins(:list_sub_4), + mixins(:list_sub_2)], + ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos') + + mixins(:list_sub_4).move_to_top + + assert_equal [mixins(:list_sub_4), + mixins(:list_sub_1), + mixins(:list_sub_3), + mixins(:list_sub_2)], + ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos') + + end + + def test_move_to_bottom_with_next_to_last_item + assert_equal [mixins(:list_sub_1), + mixins(:list_sub_2), + mixins(:list_sub_3), + mixins(:list_sub_4)], + ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos') + + mixins(:list_sub_3).move_to_bottom + + assert_equal [mixins(:list_sub_1), + mixins(:list_sub_2), + mixins(:list_sub_4), + mixins(:list_sub_3)], + ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos') + end + + def test_next_prev + assert_equal mixins(:list_sub_2), mixins(:list_sub_1).lower_item + assert_nil mixins(:list_sub_1).higher_item + assert_equal mixins(:list_sub_3), mixins(:list_sub_4).higher_item + assert_nil mixins(:list_sub_4).lower_item + end + + + def test_injection + item = ListMixin.new("parent_id"=>1) + assert_equal "parent_id = 1", item.scope_condition + assert_equal "pos", item.position_column + end + + + def test_insert_at + new = ListMixin.create("parent_id" => 20) + assert_equal 1, new.pos + + new = ListMixinSub1.create("parent_id" => 20) + assert_equal 2, new.pos + + new = ListMixinSub2.create("parent_id" => 20) + assert_equal 3, new.pos + + new4 = ListMixin.create("parent_id" => 20) + assert_equal 4, new4.pos + + new4.insert_at(3) + assert_equal 3, new4.pos + + new.reload + assert_equal 4, new.pos + + new.insert_at(2) + assert_equal 2, new.pos + + new4.reload + assert_equal 4, new4.pos + + new5 = ListMixinSub1.create("parent_id" => 20) + assert_equal 5, new5.pos + + new5.insert_at(1) + assert_equal 1, new5.pos + + new4.reload + assert_equal 5, new4.pos + end + + def test_delete_middle + assert_equal [mixins(:list_sub_1), + mixins(:list_sub_2), + mixins(:list_sub_3), + mixins(:list_sub_4)], + ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos') + + mixins(:list_sub_2).destroy + + assert_equal [mixins(:list_sub_1, :reload), + mixins(:list_sub_3, :reload), + mixins(:list_sub_4, :reload)], + ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos') + + assert_equal 1, mixins(:list_sub_1).pos + assert_equal 2, mixins(:list_sub_3).pos + assert_equal 3, mixins(:list_sub_4).pos + + mixins(:list_sub_1).destroy + + assert_equal [mixins(:list_sub_3, :reload), + mixins(:list_sub_4, :reload)], + ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos') + + assert_equal 1, mixins(:list_sub_3).pos + assert_equal 2, mixins(:list_sub_4).pos + + end + +end + diff --git a/vendor/rails/activerecord/test/modules_test.rb b/vendor/rails/activerecord/test/modules_test.rb new file mode 100644 index 00000000..6f919bbd --- /dev/null +++ b/vendor/rails/activerecord/test/modules_test.rb @@ -0,0 +1,28 @@ +require 'abstract_unit' +require 'fixtures/company_in_module' + +class ModulesTest < Test::Unit::TestCase + fixtures :accounts, :companies, :projects, :developers + + def test_module_spanning_associations + assert MyApplication::Business::Firm.find(:first).has_clients?, "Firm should have clients" + firm = MyApplication::Business::Firm.find(:first) + assert_nil firm.class.table_name.match('::'), "Firm shouldn't have the module appear in its table name" + assert_equal 2, firm.clients_count, "Firm should have two clients" + end + + def test_module_spanning_has_and_belongs_to_many_associations + project = MyApplication::Business::Project.find(:first) + project.developers << MyApplication::Business::Developer.create("name" => "John") + assert "John", project.developers.last.name + end + + def test_associations_spanning_cross_modules + account = MyApplication::Billing::Account.find(:first, :order => 'id') + assert_kind_of MyApplication::Business::Firm, account.firm + assert_kind_of MyApplication::Billing::Firm, account.qualified_billing_firm + assert_kind_of MyApplication::Billing::Firm, account.unqualified_billing_firm + assert_kind_of MyApplication::Billing::Nested::Firm, account.nested_qualified_billing_firm + assert_kind_of MyApplication::Billing::Nested::Firm, account.nested_unqualified_billing_firm + end +end diff --git a/vendor/rails/activerecord/test/multiple_db_test.rb b/vendor/rails/activerecord/test/multiple_db_test.rb new file mode 100644 index 00000000..df2ef106 --- /dev/null +++ b/vendor/rails/activerecord/test/multiple_db_test.rb @@ -0,0 +1,60 @@ +require 'abstract_unit' +require 'fixtures/entrant' + +# So we can test whether Course.connection survives a reload. +require_dependency 'fixtures/course' + +class MultipleDbTest < Test::Unit::TestCase + self.use_transactional_fixtures = false + + def setup + @courses = create_fixtures("courses") { Course.retrieve_connection } + @entrants = create_fixtures("entrants") + end + + def test_connected + assert_not_nil Entrant.connection + assert_not_nil Course.connection + end + + def test_proper_connection + assert_not_equal(Entrant.connection, Course.connection) + assert_equal(Entrant.connection, Entrant.retrieve_connection) + assert_equal(Course.connection, Course.retrieve_connection) + assert_equal(ActiveRecord::Base.connection, Entrant.connection) + end + + def test_find + c1 = Course.find(1) + assert_equal "Ruby Development", c1.name + c2 = Course.find(2) + assert_equal "Java Development", c2.name + e1 = Entrant.find(1) + assert_equal "Ruby Developer", e1.name + e2 = Entrant.find(2) + assert_equal "Ruby Guru", e2.name + e3 = Entrant.find(3) + assert_equal "Java Lover", e3.name + end + + def test_associations + c1 = Course.find(1) + assert_equal 2, c1.entrants_count + e1 = Entrant.find(1) + assert_equal e1.course.id, c1.id + c2 = Course.find(2) + assert_equal 1, c2.entrants_count + e3 = Entrant.find(3) + assert_equal e3.course.id, c2.id + end + + def test_course_connection_should_survive_dependency_reload + assert Course.connection + + Dependencies.clear + Object.send(:remove_const, :Course) + require_dependency 'fixtures/course' + + assert Course.connection + end +end diff --git a/vendor/rails/activerecord/test/pk_test.rb b/vendor/rails/activerecord/test/pk_test.rb new file mode 100644 index 00000000..839a784e --- /dev/null +++ b/vendor/rails/activerecord/test/pk_test.rb @@ -0,0 +1,80 @@ +require "#{File.dirname(__FILE__)}/abstract_unit" +require 'fixtures/topic' +require 'fixtures/subscriber' +require 'fixtures/movie' +require 'fixtures/keyboard' + +class PrimaryKeysTest < Test::Unit::TestCase + fixtures :topics, :subscribers, :movies + + def test_integer_key + topic = Topic.find(1) + assert_equal(topics(:first).author_name, topic.author_name) + topic = Topic.find(2) + assert_equal(topics(:second).author_name, topic.author_name) + + topic = Topic.new + topic.title = "New Topic" + assert_equal(nil, topic.id) + assert_nothing_raised { topic.save! } + id = topic.id + + topicReloaded = Topic.find(id) + assert_equal("New Topic", topicReloaded.title) + end + + def test_customized_primary_key_auto_assigns_on_save + Keyboard.delete_all + keyboard = Keyboard.new(:name => 'HHKB') + assert_nothing_raised { keyboard.save! } + assert_equal keyboard.id, Keyboard.find_by_name('HHKB').id + end + + def test_customized_primary_key_can_be_get_before_saving + keyboard = Keyboard.new + assert_nil keyboard.id + assert_nothing_raised { assert_nil keyboard.key_number } + end + + def test_customized_string_primary_key_settable_before_save + subscriber = Subscriber.new + assert_nothing_raised { subscriber.id = 'webster123' } + assert_equal 'webster123', subscriber.id + assert_equal 'webster123', subscriber.nick + end + + def test_string_key + subscriber = Subscriber.find(subscribers(:first).nick) + assert_equal(subscribers(:first).name, subscriber.name) + subscriber = Subscriber.find(subscribers(:second).nick) + assert_equal(subscribers(:second).name, subscriber.name) + + subscriber = Subscriber.new + subscriber.id = "jdoe" + assert_equal("jdoe", subscriber.id) + subscriber.name = "John Doe" + assert_nothing_raised { subscriber.save! } + assert_equal("jdoe", subscriber.id) + + subscriberReloaded = Subscriber.find("jdoe") + assert_equal("John Doe", subscriberReloaded.name) + end + + def test_find_with_more_than_one_string_key + assert_equal 2, Subscriber.find(subscribers(:first).nick, subscribers(:second).nick).length + end + + def test_primary_key_prefix + ActiveRecord::Base.primary_key_prefix_type = :table_name + Topic.reset_primary_key + assert_equal "topicid", Topic.primary_key + + ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore + Topic.reset_primary_key + assert_equal "topic_id", Topic.primary_key + + ActiveRecord::Base.primary_key_prefix_type = nil + Topic.reset_primary_key + assert_equal "id", Topic.primary_key + end +end diff --git a/vendor/rails/activerecord/test/readonly_test.rb b/vendor/rails/activerecord/test/readonly_test.rb new file mode 100755 index 00000000..584eb735 --- /dev/null +++ b/vendor/rails/activerecord/test/readonly_test.rb @@ -0,0 +1,107 @@ +require 'abstract_unit' +require 'fixtures/post' +require 'fixtures/comment' +require 'fixtures/developer' +require 'fixtures/project' +require 'fixtures/reader' +require 'fixtures/person' + +# Dummy class methods to test implicit association scoping. +def Comment.foo() find :first end +def Project.foo() find :first end + + +class ReadOnlyTest < Test::Unit::TestCase + fixtures :posts, :comments, :developers, :projects, :developers_projects + + def test_cant_save_readonly_record + dev = Developer.find(1) + assert !dev.readonly? + + dev.readonly! + assert dev.readonly? + + assert_nothing_raised do + dev.name = 'Luscious forbidden fruit.' + assert !dev.save + dev.name = 'Forbidden.' + end + assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save } + assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save! } + end + + + def test_find_with_readonly_option + Developer.find(:all).each { |d| assert !d.readonly? } + Developer.find(:all, :readonly => false).each { |d| assert !d.readonly? } + Developer.find(:all, :readonly => true).each { |d| assert d.readonly? } + end + + + def test_find_with_joins_option_implies_readonly + # Blank joins don't count. + Developer.find(:all, :joins => ' ').each { |d| assert !d.readonly? } + Developer.find(:all, :joins => ' ', :readonly => false).each { |d| assert !d.readonly? } + + # Others do. + Developer.find(:all, :joins => ', projects').each { |d| assert d.readonly? } + Developer.find(:all, :joins => ', projects', :readonly => false).each { |d| assert !d.readonly? } + end + + + def test_habtm_find_readonly + dev = Developer.find(1) + assert !dev.projects.empty? + assert dev.projects.all?(&:readonly?) + assert dev.projects.find(:all).all?(&:readonly?) + assert dev.projects.find(:all, :readonly => true).all?(&:readonly?) + end + + def test_has_many_find_readonly + post = Post.find(1) + assert !post.comments.empty? + assert !post.comments.any?(&:readonly?) + assert !post.comments.find(:all).any?(&:readonly?) + assert post.comments.find(:all, :readonly => true).all?(&:readonly?) + end + + def test_has_many_with_through_is_not_implicitly_marked_readonly + assert people = Post.find(1).people + assert !people.any?(&:readonly?) + end + + def test_readonly_scoping + Post.with_scope(:find => { :conditions => '1=1' }) do + assert !Post.find(1).readonly? + assert Post.find(1, :readonly => true).readonly? + assert !Post.find(1, :readonly => false).readonly? + end + + Post.with_scope(:find => { :joins => ' ' }) do + assert !Post.find(1).readonly? + assert Post.find(1, :readonly => true).readonly? + assert !Post.find(1, :readonly => false).readonly? + end + + # Oracle barfs on this because the join includes unqualified and + # conflicting column names + unless current_adapter?(:OracleAdapter) + Post.with_scope(:find => { :joins => ', developers' }) do + assert Post.find(1).readonly? + assert Post.find(1, :readonly => true).readonly? + assert !Post.find(1, :readonly => false).readonly? + end + end + + Post.with_scope(:find => { :readonly => true }) do + assert Post.find(1).readonly? + assert Post.find(1, :readonly => true).readonly? + assert !Post.find(1, :readonly => false).readonly? + end + end + + def test_association_collection_method_missing_scoping_not_readonly + assert !Developer.find(1).projects.foo.readonly? + assert !Post.find(1).comments.foo.readonly? + end +end diff --git a/vendor/rails/activerecord/test/reflection_test.rb b/vendor/rails/activerecord/test/reflection_test.rb new file mode 100644 index 00000000..64689595 --- /dev/null +++ b/vendor/rails/activerecord/test/reflection_test.rb @@ -0,0 +1,153 @@ +require 'abstract_unit' +require 'fixtures/topic' +require 'fixtures/customer' +require 'fixtures/company' +require 'fixtures/company_in_module' +require 'fixtures/subscriber' + +class ReflectionTest < Test::Unit::TestCase + fixtures :topics, :customers, :companies, :subscribers + + def setup + @first = Topic.find(1) + end + + def test_column_null_not_null + subscriber = Subscriber.find(:first) + assert subscriber.column_for_attribute("name").null + assert !subscriber.column_for_attribute("nick").null + end + + def test_read_attribute_names + assert_equal( + %w( id title author_name author_email_address bonus_time written_on last_read content approved replies_count parent_id type ).sort, + @first.attribute_names + ) + end + + def test_columns + assert_equal 12, Topic.columns.length + end + + def test_columns_are_returned_in_the_order_they_were_declared + column_names = Topic.columns.map { |column| column.name } + assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content approved replies_count parent_id type), column_names + end + + def test_content_columns + content_columns = Topic.content_columns + content_column_names = content_columns.map {|column| column.name} + assert_equal 8, content_columns.length + assert_equal %w(title author_name author_email_address written_on bonus_time last_read content approved).sort, content_column_names.sort + end + + def test_column_string_type_and_limit + assert_equal :string, @first.column_for_attribute("title").type + assert_equal 255, @first.column_for_attribute("title").limit + end + + def test_human_name_for_column + assert_equal "Author name", @first.column_for_attribute("author_name").human_name + end + + def test_integer_columns + assert_equal :integer, @first.column_for_attribute("id").type + end + + def test_aggregation_reflection + reflection_for_address = ActiveRecord::Reflection::AggregateReflection.new( + :composed_of, :address, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer + ) + + reflection_for_balance = ActiveRecord::Reflection::AggregateReflection.new( + :composed_of, :balance, { :class_name => "Money", :mapping => %w(balance amount) }, Customer + ) + + reflection_for_gps_location = ActiveRecord::Reflection::AggregateReflection.new( + :composed_of, :gps_location, { }, Customer + ) + + assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location) + assert Customer.reflect_on_all_aggregations.include?(reflection_for_balance) + assert Customer.reflect_on_all_aggregations.include?(reflection_for_address) + + assert_equal reflection_for_address, Customer.reflect_on_aggregation(:address) + + assert_equal Address, Customer.reflect_on_aggregation(:address).klass + + assert_equal Money, Customer.reflect_on_aggregation(:balance).klass + end + + def test_has_many_reflection + reflection_for_clients = ActiveRecord::Reflection::AssociationReflection.new(:has_many, :clients, { :order => "id", :dependent => :destroy }, Firm) + + assert_equal reflection_for_clients, Firm.reflect_on_association(:clients) + + assert_equal Client, Firm.reflect_on_association(:clients).klass + assert_equal 'companies', Firm.reflect_on_association(:clients).table_name + + assert_equal Client, Firm.reflect_on_association(:clients_of_firm).klass + assert_equal 'companies', Firm.reflect_on_association(:clients_of_firm).table_name + end + + def test_has_one_reflection + reflection_for_account = ActiveRecord::Reflection::AssociationReflection.new(:has_one, :account, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) + assert_equal reflection_for_account, Firm.reflect_on_association(:account) + + assert_equal Account, Firm.reflect_on_association(:account).klass + assert_equal 'accounts', Firm.reflect_on_association(:account).table_name + end + + def test_association_reflection_in_modules + assert_reflection MyApplication::Business::Firm, + :clients_of_firm, + :klass => MyApplication::Business::Client, + :class_name => 'Client', + :table_name => 'companies' + + assert_reflection MyApplication::Billing::Account, + :firm, + :klass => MyApplication::Business::Firm, + :class_name => 'MyApplication::Business::Firm', + :table_name => 'companies' + + assert_reflection MyApplication::Billing::Account, + :qualified_billing_firm, + :klass => MyApplication::Billing::Firm, + :class_name => 'MyApplication::Billing::Firm', + :table_name => 'companies' + + assert_reflection MyApplication::Billing::Account, + :unqualified_billing_firm, + :klass => MyApplication::Billing::Firm, + :class_name => 'Firm', + :table_name => 'companies' + + assert_reflection MyApplication::Billing::Account, + :nested_qualified_billing_firm, + :klass => MyApplication::Billing::Nested::Firm, + :class_name => 'MyApplication::Billing::Nested::Firm', + :table_name => 'companies' + + assert_reflection MyApplication::Billing::Account, + :nested_unqualified_billing_firm, + :klass => MyApplication::Billing::Nested::Firm, + :class_name => 'Nested::Firm', + :table_name => 'companies' + end + + def test_reflection_of_all_associations + assert_equal 13, Firm.reflect_on_all_associations.size + assert_equal 11, Firm.reflect_on_all_associations(:has_many).size + assert_equal 2, Firm.reflect_on_all_associations(:has_one).size + assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size + end + + private + def assert_reflection(klass, association, options) + assert reflection = klass.reflect_on_association(association) + options.each do |method, value| + assert_equal(value, reflection.send(method)) + end + end +end diff --git a/vendor/rails/activerecord/test/schema_dumper_test.rb b/vendor/rails/activerecord/test/schema_dumper_test.rb new file mode 100644 index 00000000..5dd0d4e9 --- /dev/null +++ b/vendor/rails/activerecord/test/schema_dumper_test.rb @@ -0,0 +1,60 @@ +require 'abstract_unit' +require "#{File.dirname(__FILE__)}/../lib/active_record/schema_dumper" +require 'stringio' + +if ActiveRecord::Base.connection.respond_to?(:tables) + + class SchemaDumperTest < Test::Unit::TestCase + def test_schema_dump + stream = StringIO.new + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + output = stream.string + + assert_match %r{create_table "accounts"}, output + assert_match %r{create_table "authors"}, output + assert_no_match %r{create_table "schema_info"}, output + end + + def test_schema_dump_includes_not_null_columns + stream = StringIO.new + + ActiveRecord::SchemaDumper.ignore_tables = [/^[^s]/] + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + output = stream.string + assert_match %r{:null => false}, output + end + + def test_schema_dump_with_string_ignored_table + stream = StringIO.new + + ActiveRecord::SchemaDumper.ignore_tables = ['accounts'] + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + output = stream.string + assert_no_match %r{create_table "accounts"}, output + assert_match %r{create_table "authors"}, output + assert_no_match %r{create_table "schema_info"}, output + end + + + def test_schema_dump_with_regexp_ignored_table + stream = StringIO.new + + ActiveRecord::SchemaDumper.ignore_tables = [/^account/] + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + output = stream.string + assert_no_match %r{create_table "accounts"}, output + assert_match %r{create_table "authors"}, output + assert_no_match %r{create_table "schema_info"}, output + end + + + def test_schema_dump_illegal_ignored_table_value + stream = StringIO.new + ActiveRecord::SchemaDumper.ignore_tables = [5] + assert_raise(StandardError) do + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + end + end + end + +end diff --git a/vendor/rails/activerecord/test/schema_test_postgresql.rb b/vendor/rails/activerecord/test/schema_test_postgresql.rb new file mode 100644 index 00000000..c743a861 --- /dev/null +++ b/vendor/rails/activerecord/test/schema_test_postgresql.rb @@ -0,0 +1,64 @@ +require 'abstract_unit' + +class SchemaTest < Test::Unit::TestCase + self.use_transactional_fixtures = false + + SCHEMA_NAME = 'test_schema' + TABLE_NAME = 'things' + COLUMNS = [ + 'id integer', + 'name character varying(50)', + 'moment timestamp without time zone default now()' + ] + + def setup + @connection = ActiveRecord::Base.connection + @connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})" + end + + def teardown + @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE" + end + + def test_with_schema_prefixed_table_name + assert_nothing_raised do + assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{TABLE_NAME}") + end + end + + def test_with_schema_search_path + assert_nothing_raised do + with_schema_search_path(SCHEMA_NAME) do + assert_equal COLUMNS, columns(TABLE_NAME) + end + end + end + + def test_raise_on_unquoted_schema_name + assert_raise(ActiveRecord::StatementInvalid) do + with_schema_search_path '$user,public' + end + end + + def test_without_schema_search_path + assert_raise(ActiveRecord::StatementInvalid) { columns(TABLE_NAME) } + end + + def test_ignore_nil_schema_search_path + assert_nothing_raised { with_schema_search_path nil } + end + + private + def columns(table_name) + @connection.send(:column_definitions, table_name).map do |name, type, default| + "#{name} #{type}" + (default ? " default #{default}" : '') + end + end + + def with_schema_search_path(schema_search_path) + @connection.schema_search_path = schema_search_path + yield if block_given? + ensure + @connection.schema_search_path = "'$user', public" + end +end diff --git a/vendor/rails/activerecord/test/synonym_test_oracle.rb b/vendor/rails/activerecord/test/synonym_test_oracle.rb new file mode 100644 index 00000000..fcfb2f3b --- /dev/null +++ b/vendor/rails/activerecord/test/synonym_test_oracle.rb @@ -0,0 +1,17 @@ +require 'abstract_unit' +require 'fixtures/topic' +require 'fixtures/subject' + +# confirm that synonyms work just like tables; in this case +# the "subjects" table in Oracle (defined in oci.sql) is just +# a synonym to the "topics" table + +class TestOracleSynonym < Test::Unit::TestCase + + def test_oracle_synonym + topic = Topic.new + subject = Subject.new + assert_equal(topic.attributes, subject.attributes) + end + +end diff --git a/vendor/rails/activerecord/test/threaded_connections_test.rb b/vendor/rails/activerecord/test/threaded_connections_test.rb new file mode 100644 index 00000000..d9cc47ee --- /dev/null +++ b/vendor/rails/activerecord/test/threaded_connections_test.rb @@ -0,0 +1,45 @@ +require 'abstract_unit' +require 'fixtures/topic' + +class ThreadedConnectionsTest < Test::Unit::TestCase + self.use_transactional_fixtures = false + + fixtures :topics + + def setup + @connection = ActiveRecord::Base.remove_connection + @connections = [] + @allow_concurrency = ActiveRecord::Base.allow_concurrency + end + + def teardown + # clear the connection cache + ActiveRecord::Base.send(:clear_all_cached_connections!) + # set allow_concurrency to saved value + ActiveRecord::Base.allow_concurrency = @allow_concurrency + # reestablish old connection + ActiveRecord::Base.establish_connection(@connection) + end + + def gather_connections(use_threaded_connections) + ActiveRecord::Base.allow_concurrency = use_threaded_connections + ActiveRecord::Base.establish_connection(@connection) + + 5.times do + Thread.new do + Topic.find :first + @connections << ActiveRecord::Base.active_connections.values.first + end.join + end + end + + def test_threaded_connections + gather_connections(true) + assert_equal @connections.uniq.length, 5 + end + + def test_unthreaded_connections + gather_connections(false) + assert_equal @connections.uniq.length, 1 + end +end diff --git a/vendor/rails/activerecord/test/transactions_test.rb b/vendor/rails/activerecord/test/transactions_test.rb new file mode 100644 index 00000000..0048e24b --- /dev/null +++ b/vendor/rails/activerecord/test/transactions_test.rb @@ -0,0 +1,216 @@ +require 'abstract_unit' +require 'fixtures/topic' +require 'fixtures/developer' + +class TransactionTest < Test::Unit::TestCase + self.use_transactional_fixtures = false + + fixtures :topics, :developers + + def setup + # sqlite does not seem to return these in the right order, so we sort them + # explicitly for sqlite's sake. sqlite3 does fine. + @first, @second = Topic.find(1, 2).sort_by { |t| t.id } + end + + def test_successful + Topic.transaction do + @first.approved = true + @second.approved = false + @first.save + @second.save + end + + assert Topic.find(1).approved?, "First should have been approved" + assert !Topic.find(2).approved?, "Second should have been unapproved" + end + + def transaction_with_return + Topic.transaction do + @first.approved = true + @second.approved = false + @first.save + @second.save + return + end + end + + def test_successful_with_return + class << Topic.connection + alias :real_commit_db_transaction :commit_db_transaction + def commit_db_transaction + $committed = true + real_commit_db_transaction + end + end + + $committed = false + transaction_with_return + assert $committed + + assert Topic.find(1).approved?, "First should have been approved" + assert !Topic.find(2).approved?, "Second should have been unapproved" + ensure + class << Topic.connection + alias :commit_db_transaction :real_commit_db_transaction rescue nil + end + end + + def test_successful_with_instance_method + @first.transaction do + @first.approved = true + @second.approved = false + @first.save + @second.save + end + + assert Topic.find(1).approved?, "First should have been approved" + assert !Topic.find(2).approved?, "Second should have been unapproved" + end + + def test_failing_on_exception + begin + Topic.transaction do + @first.approved = true + @second.approved = false + @first.save + @second.save + raise "Bad things!" + end + rescue + # caught it + end + + assert @first.approved?, "First should still be changed in the objects" + assert !@second.approved?, "Second should still be changed in the objects" + + assert !Topic.find(1).approved?, "First shouldn't have been approved" + assert Topic.find(2).approved?, "Second should still be approved" + end + + def test_failing_with_object_rollback + assert !@first.approved?, "First should be unapproved initially" + + begin + Topic.transaction(@first, @second) do + @first.approved = true + @second.approved = false + @first.save + @second.save + raise "Bad things!" + end + rescue + # caught it + end + + assert !@first.approved?, "First shouldn't have been approved" + assert @second.approved?, "Second should still be approved" + end + + def test_callback_rollback_in_save + add_exception_raising_after_save_callback_to_topic + + begin + @first.approved = true + @first.save + flunk + rescue => e + assert_equal "Make the transaction rollback", e.message + assert !Topic.find(1).approved? + ensure + remove_exception_raising_after_save_callback_to_topic + end + end + + def test_nested_explicit_transactions + Topic.transaction do + Topic.transaction do + @first.approved = true + @second.approved = false + @first.save + @second.save + end + end + + assert Topic.find(1).approved?, "First should have been approved" + assert !Topic.find(2).approved?, "Second should have been unapproved" + end + + # This will cause transactions to overlap and fail unless they are + # performed on separate database connections. + def test_transaction_per_thread + assert_nothing_raised do + threads = (1..20).map do + Thread.new do + Topic.transaction do + topic = Topic.find(:first) + topic.approved = !topic.approved? + topic.save! + topic.approved = !topic.approved? + topic.save! + end + end + end + + threads.each { |t| t.join } + end + end + + # Test for dirty reads among simultaneous transactions. + def test_transaction_isolation__read_committed + # Should be invariant. + original_salary = Developer.find(1).salary + temporary_salary = 200000 + + assert_nothing_raised do + threads = (1..20).map do + Thread.new do + Developer.transaction do + # Expect original salary. + dev = Developer.find(1) + assert_equal original_salary, dev.salary + + dev.salary = temporary_salary + dev.save! + + # Expect temporary salary. + dev = Developer.find(1) + assert_equal temporary_salary, dev.salary + + dev.salary = original_salary + dev.save! + + # Expect original salary. + dev = Developer.find(1) + assert_equal original_salary, dev.salary + end + end + end + + # Keep our eyes peeled. + threads << Thread.new do + 10.times do + sleep 0.05 + Developer.transaction do + # Always expect original salary. + assert_equal original_salary, Developer.find(1).salary + end + end + end + + threads.each { |t| t.join } + end + + assert_equal original_salary, Developer.find(1).salary + end + + + private + def add_exception_raising_after_save_callback_to_topic + Topic.class_eval { def after_save() raise "Make the transaction rollback" end } + end + + def remove_exception_raising_after_save_callback_to_topic + Topic.class_eval { remove_method :after_save } + end +end diff --git a/vendor/rails/activerecord/test/unconnected_test.rb b/vendor/rails/activerecord/test/unconnected_test.rb new file mode 100755 index 00000000..2d06410d --- /dev/null +++ b/vendor/rails/activerecord/test/unconnected_test.rb @@ -0,0 +1,32 @@ +require 'abstract_unit' + +class TestRecord < ActiveRecord::Base +end + +class TestUnconnectedAdaptor < Test::Unit::TestCase + self.use_transactional_fixtures = false + + def setup + @underlying = ActiveRecord::Base.connection + @specification = ActiveRecord::Base.remove_connection + end + + def teardown + @underlying = nil + ActiveRecord::Base.establish_connection(@specification) + end + + def test_connection_no_longer_established + assert_raise(ActiveRecord::ConnectionNotEstablished) do + TestRecord.find(1) + end + + assert_raise(ActiveRecord::ConnectionNotEstablished) do + TestRecord.new.save + end + end + + def test_underlying_adapter_no_longer_active + assert !@underlying.active?, "Removed adapter should no longer be active" + end +end diff --git a/vendor/rails/activerecord/test/validations_test.rb b/vendor/rails/activerecord/test/validations_test.rb new file mode 100755 index 00000000..d7ba7459 --- /dev/null +++ b/vendor/rails/activerecord/test/validations_test.rb @@ -0,0 +1,1027 @@ +require 'abstract_unit' +require 'fixtures/topic' +require 'fixtures/reply' +require 'fixtures/developer' + +# The following methods in Topic are used in test_conditional_validation_* +class Topic + def condition_is_true + return true + end + + def condition_is_true_but_its_not + return false + end +end + +class ValidationsTest < Test::Unit::TestCase + fixtures :topics, :developers + + def setup + Topic.write_inheritable_attribute(:validate, nil) + Topic.write_inheritable_attribute(:validate_on_create, nil) + Topic.write_inheritable_attribute(:validate_on_update, nil) + end + + def test_single_field_validation + r = Reply.new + r.title = "There's no content!" + assert !r.save, "A reply without content shouldn't be saveable" + + r.content = "Messa content!" + assert r.save, "A reply with content should be saveable" + end + + def test_single_attr_validation_and_error_msg + r = Reply.new + r.title = "There's no content!" + r.save + assert r.errors.invalid?("content"), "A reply without content should mark that attribute as invalid" + assert_equal "Empty", r.errors.on("content"), "A reply without content should contain an error" + assert_equal 1, r.errors.count + end + + def test_double_attr_validation_and_error_msg + r = Reply.new + assert !r.save + + assert r.errors.invalid?("title"), "A reply without title should mark that attribute as invalid" + assert_equal "Empty", r.errors.on("title"), "A reply without title should contain an error" + + assert r.errors.invalid?("content"), "A reply without content should mark that attribute as invalid" + assert_equal "Empty", r.errors.on("content"), "A reply without content should contain an error" + + assert_equal 2, r.errors.count + end + + def test_error_on_create + r = Reply.new + r.title = "Wrong Create" + assert !r.save + assert r.errors.invalid?("title"), "A reply with a bad title should mark that attribute as invalid" + assert_equal "is Wrong Create", r.errors.on("title"), "A reply with a bad content should contain an error" + end + + def test_error_on_update + r = Reply.new + r.title = "Bad" + r.content = "Good" + assert r.save, "First save should be successful" + + r.title = "Wrong Update" + assert !r.save, "Second save should fail" + + assert r.errors.invalid?("title"), "A reply with a bad title should mark that attribute as invalid" + assert_equal "is Wrong Update", r.errors.on("title"), "A reply with a bad content should contain an error" + end + + def test_invalid_record_exception + assert_raises(ActiveRecord::RecordInvalid) { Reply.create! } + assert_raises(ActiveRecord::RecordInvalid) { Reply.new.save! } + + begin + r = Reply.new + r.save! + flunk + rescue ActiveRecord::RecordInvalid => invalid + assert_equal r, invalid.record + end + end + + def test_single_error_per_attr_iteration + r = Reply.new + r.save + + errors = [] + r.errors.each { |attr, msg| errors << [attr, msg] } + + assert errors.include?(["title", "Empty"]) + assert errors.include?(["content", "Empty"]) + end + + def test_multiple_errors_per_attr_iteration_with_full_error_composition + r = Reply.new + r.title = "Wrong Create" + r.content = "Mismatch" + r.save + + errors = [] + r.errors.each_full { |error| errors << error } + + assert_equal "Title is Wrong Create", errors[0] + assert_equal "Title is Content Mismatch", errors[1] + assert_equal 2, r.errors.count + end + + def test_errors_on_base + r = Reply.new + r.content = "Mismatch" + r.save + r.errors.add_to_base "Reply is not dignifying" + + errors = [] + r.errors.each_full { |error| errors << error } + + assert_equal "Reply is not dignifying", r.errors.on_base + + assert errors.include?("Title Empty") + assert errors.include?("Reply is not dignifying") + assert_equal 2, r.errors.count + end + + def test_create_without_validation + reply = Reply.new + assert !reply.save + assert reply.save(false) + end + + def test_validates_each + perform = true + hits = 0 + Topic.validates_each(:title, :content, [:title, :content]) do |record, attr| + if perform + record.errors.add attr, 'gotcha' + hits += 1 + end + end + t = Topic.new("title" => "valid", "content" => "whatever") + assert !t.save + assert_equal 4, hits + assert_equal %w(gotcha gotcha), t.errors.on(:title) + assert_equal %w(gotcha gotcha), t.errors.on(:content) + ensure + perform = false + end + + def test_errors_on_boundary_breaking + developer = Developer.new("name" => "xs") + assert !developer.save + assert_equal "is too short (minimum is 3 characters)", developer.errors.on("name") + + developer.name = "All too very long for this boundary, it really is" + assert !developer.save + assert_equal "is too long (maximum is 20 characters)", developer.errors.on("name") + + developer.name = "Just right" + assert developer.save + end + + def test_title_confirmation_no_confirm + Topic.validates_confirmation_of(:title) + + t = Topic.create("title" => "We should not be confirmed") + assert t.save + end + + def test_title_confirmation + Topic.validates_confirmation_of(:title) + + t = Topic.create("title" => "We should be confirmed","title_confirmation" => "") + assert !t.save + + t.title_confirmation = "We should be confirmed" + assert t.save + end + + def test_terms_of_service_agreement_no_acceptance + Topic.validates_acceptance_of(:terms_of_service, :on => :create) + + t = Topic.create("title" => "We should not be confirmed") + assert t.save + end + + def test_terms_of_service_agreement + Topic.validates_acceptance_of(:terms_of_service, :on => :create) + + t = Topic.create("title" => "We should be confirmed","terms_of_service" => "") + assert !t.save + assert_equal "must be accepted", t.errors.on(:terms_of_service) + + t.terms_of_service = "1" + assert t.save + end + + + def test_eula + Topic.validates_acceptance_of(:eula, :message => "must be abided", :on => :create) + + t = Topic.create("title" => "We should be confirmed","eula" => "") + assert !t.save + assert_equal "must be abided", t.errors.on(:eula) + + t.eula = "1" + assert t.save + end + + def test_terms_of_service_agreement_with_accept_value + Topic.validates_acceptance_of(:terms_of_service, :on => :create, :accept => "I agree.") + + t = Topic.create("title" => "We should be confirmed", "terms_of_service" => "") + assert !t.save + assert_equal "must be accepted", t.errors.on(:terms_of_service) + + t.terms_of_service = "I agree." + assert t.save + end + + def test_validate_presences + Topic.validates_presence_of(:title, :content) + + t = Topic.create + assert !t.save + assert_equal "can't be blank", t.errors.on(:title) + assert_equal "can't be blank", t.errors.on(:content) + + t.title = "something" + t.content = " " + + assert !t.save + assert_equal "can't be blank", t.errors.on(:content) + + t.content = "like stuff" + + assert t.save + end + + def test_validate_uniqueness + Topic.validates_uniqueness_of(:title) + + t = Topic.new("title" => "I'm unique!") + assert t.save, "Should save t as unique" + + t.content = "Remaining unique" + assert t.save, "Should still save t as unique" + + t2 = Topic.new("title" => "I'm unique!") + assert !t2.valid?, "Shouldn't be valid" + assert !t2.save, "Shouldn't save t2 as unique" + assert_equal "has already been taken", t2.errors.on(:title) + + t2.title = "Now Im really also unique" + assert t2.save, "Should now save t2 as unique" + end + + def test_validate_uniqueness_with_scope + Reply.validates_uniqueness_of(:content, :scope => "parent_id") + + t = Topic.create("title" => "I'm unique!") + + r1 = t.replies.create "title" => "r1", "content" => "hello world" + assert r1.valid?, "Saving r1" + + r2 = t.replies.create "title" => "r2", "content" => "hello world" + assert !r2.valid?, "Saving r2 first time" + + r2.content = "something else" + assert r2.save, "Saving r2 second time" + + t2 = Topic.create("title" => "I'm unique too!") + r3 = t2.replies.create "title" => "r3", "content" => "hello world" + assert r3.valid?, "Saving r3" + end + + def test_validate_uniqueness_with_scope_array + Reply.validates_uniqueness_of(:author_name, :scope => [:author_email_address, :parent_id]) + + t = Topic.create("title" => "The earth is actually flat!") + + r1 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply" + assert r1.valid?, "Saving r1" + + r2 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply again..." + assert !r2.valid?, "Saving r2. Double reply by same author." + + r2.author_email_address = "jeremy_alt_email@rubyonrails.com" + assert r2.save, "Saving r2 the second time." + + r3 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy_alt_email@rubyonrails.com", "title" => "You're wrong", "content" => "It's cubic" + assert !r3.valid?, "Saving r3" + + r3.author_name = "jj" + assert r3.save, "Saving r3 the second time." + + r3.author_name = "jeremy" + assert !r3.save, "Saving r3 the third time." + end + + def test_validate_format + Topic.validates_format_of(:title, :content, :with => /^Validation\smacros \w+!$/, :message => "is bad data") + + t = Topic.create("title" => "i'm incorrect", "content" => "Validation macros rule!") + assert !t.valid?, "Shouldn't be valid" + assert !t.save, "Shouldn't save because it's invalid" + assert_equal "is bad data", t.errors.on(:title) + assert_nil t.errors.on(:content) + + t.title = "Validation macros rule!" + + assert t.save + assert_nil t.errors.on(:title) + + assert_raise(ArgumentError) { Topic.validates_format_of(:title, :content) } + end + + # testing ticket #3142 + def test_validate_format_numeric + Topic.validates_format_of(:title, :content, :with => /^[1-9][0-9]*$/, :message => "is bad data") + + t = Topic.create("title" => "72x", "content" => "6789") + assert !t.valid?, "Shouldn't be valid" + assert !t.save, "Shouldn't save because it's invalid" + assert_equal "is bad data", t.errors.on(:title) + assert_nil t.errors.on(:content) + + t.title = "-11" + assert !t.valid?, "Shouldn't be valid" + + t.title = "03" + assert !t.valid?, "Shouldn't be valid" + + t.title = "z44" + assert !t.valid?, "Shouldn't be valid" + + t.title = "5v7" + assert !t.valid?, "Shouldn't be valid" + + t.title = "1" + + assert t.save + assert_nil t.errors.on(:title) + end + + def test_validates_inclusion_of + Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ) ) + + assert !Topic.create("title" => "a!", "content" => "abc").valid? + assert !Topic.create("title" => "a b", "content" => "abc").valid? + assert !Topic.create("title" => nil, "content" => "def").valid? + + t = Topic.create("title" => "a", "content" => "I know you are but what am I?") + assert t.valid? + t.title = "uhoh" + assert !t.valid? + assert t.errors.on(:title) + assert_equal "is not included in the list", t.errors["title"] + + assert_raise(ArgumentError) { Topic.validates_inclusion_of( :title, :in => nil ) } + assert_raise(ArgumentError) { Topic.validates_inclusion_of( :title, :in => 0) } + + assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => "hi!" ) } + assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => {} ) } + assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => [] ) } + end + + def test_validates_inclusion_of_with_allow_nil + Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :allow_nil=>true ) + + assert !Topic.create("title" => "a!", "content" => "abc").valid? + assert !Topic.create("title" => "", "content" => "abc").valid? + assert Topic.create("title" => nil, "content" => "abc").valid? + end + + def test_numericality_with_allow_nil_and_getter_method + Developer.validates_numericality_of( :salary, :allow_nil => true) + developer = Developer.new("name" => "michael", "salary" => nil) + developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end") + assert developer.valid? + end + + def test_validates_exclusion_of + Topic.validates_exclusion_of( :title, :in => %w( abe monkey ) ) + + assert Topic.create("title" => "something", "content" => "abc").valid? + assert !Topic.create("title" => "monkey", "content" => "abc").valid? + end + + def test_validates_length_of_using_minimum + Topic.validates_length_of :title, :minimum => 5 + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = "not" + assert !t.valid? + assert t.errors.on(:title) + assert_equal "is too short (minimum is 5 characters)", t.errors["title"] + + t.title = "" + assert !t.valid? + assert t.errors.on(:title) + assert_equal "is too short (minimum is 5 characters)", t.errors["title"] + + t.title = nil + assert !t.valid? + assert t.errors.on(:title) + assert_equal "is too short (minimum is 5 characters)", t.errors["title"] + end + + def test_optionally_validates_length_of_using_minimum + Topic.validates_length_of :title, :minimum => 5, :allow_nil => true + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = nil + assert t.valid? + end + + def test_validates_length_of_using_maximum + Topic.validates_length_of :title, :maximum => 5 + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = "notvalid" + assert !t.valid? + assert t.errors.on(:title) + assert_equal "is too long (maximum is 5 characters)", t.errors["title"] + + t.title = "" + assert t.valid? + + t.title = nil + assert !t.valid? + end + + def test_optionally_validates_length_of_using_maximum + Topic.validates_length_of :title, :maximum => 5, :allow_nil => true + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = nil + assert t.valid? + end + + def test_validates_length_of_using_within + Topic.validates_length_of(:title, :content, :within => 3..5) + + t = Topic.new("title" => "a!", "content" => "I'm ooooooooh so very long") + assert !t.valid? + assert_equal "is too short (minimum is 3 characters)", t.errors.on(:title) + assert_equal "is too long (maximum is 5 characters)", t.errors.on(:content) + + t.title = nil + t.content = nil + assert !t.valid? + assert_equal "is too short (minimum is 3 characters)", t.errors.on(:title) + assert_equal "is too short (minimum is 3 characters)", t.errors.on(:content) + + t.title = "abe" + t.content = "mad" + assert t.valid? + end + + def test_optionally_validates_length_of_using_within + Topic.validates_length_of :title, :content, :within => 3..5, :allow_nil => true + + t = Topic.create('title' => 'abc', 'content' => 'abcd') + assert t.valid? + + t.title = nil + assert t.valid? + end + + def test_optionally_validates_length_of_using_within_on_create + Topic.validates_length_of :title, :content, :within => 5..10, :on => :create, :too_long => "my string is too long: %d" + + t = Topic.create("title" => "thisisnotvalid", "content" => "whatever") + assert !t.save + assert t.errors.on(:title) + assert_equal "my string is too long: 10", t.errors[:title] + + t.title = "butthisis" + assert t.save + + t.title = "few" + assert t.save + + t.content = "andthisislong" + assert t.save + + t.content = t.title = "iamfine" + assert t.save + end + + def test_optionally_validates_length_of_using_within_on_update + Topic.validates_length_of :title, :content, :within => 5..10, :on => :update, :too_short => "my string is too short: %d" + + t = Topic.create("title" => "vali", "content" => "whatever") + assert !t.save + assert t.errors.on(:title) + + t.title = "not" + assert !t.save + assert t.errors.on(:title) + assert_equal "my string is too short: 5", t.errors[:title] + + t.title = "valid" + t.content = "andthisistoolong" + assert !t.save + assert t.errors.on(:content) + + t.content = "iamfine" + assert t.save + end + + def test_validates_length_of_using_is + Topic.validates_length_of :title, :is => 5 + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = "notvalid" + assert !t.valid? + assert t.errors.on(:title) + assert_equal "is the wrong length (should be 5 characters)", t.errors["title"] + + t.title = "" + assert !t.valid? + + t.title = nil + assert !t.valid? + end + + def test_optionally_validates_length_of_using_is + Topic.validates_length_of :title, :is => 5, :allow_nil => true + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = nil + assert t.valid? + end + + def test_validates_length_of_using_bignum + bigmin = 2 ** 30 + bigmax = 2 ** 32 + bigrange = bigmin...bigmax + assert_nothing_raised do + Topic.validates_length_of :title, :is => bigmin + 5 + Topic.validates_length_of :title, :within => bigrange + Topic.validates_length_of :title, :in => bigrange + Topic.validates_length_of :title, :minimum => bigmin + Topic.validates_length_of :title, :maximum => bigmax + end + end + + def test_validates_length_with_globaly_modified_error_message + ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d' + Topic.validates_length_of :title, :minimum => 10 + t = Topic.create(:title => 'too short') + assert !t.valid? + + assert_equal 'tu est trops petit hombre 10', t.errors['title'] + end + + def test_validates_size_of_association + assert_nothing_raised { Topic.validates_size_of :replies, :minimum => 1 } + t = Topic.new('title' => 'noreplies', 'content' => 'whatever') + assert !t.save + assert t.errors.on(:replies) + t.replies.create('title' => 'areply', 'content' => 'whateveragain') + assert t.valid? + end + + def test_validates_length_of_nasty_params + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum=>6, :maximum=>9) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :maximum=>9) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :minimum=>9) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :is=>9) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum=>"a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :maximum=>"a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>"a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :is=>"a") } + end + + def test_validates_length_of_custom_errors_for_minimum_with_message + Topic.validates_length_of( :title, :minimum=>5, :message=>"boo %d" ) + t = Topic.create("title" => "uhoh", "content" => "whatever") + assert !t.valid? + assert t.errors.on(:title) + assert_equal "boo 5", t.errors["title"] + end + + def test_validates_length_of_custom_errors_for_minimum_with_too_short + Topic.validates_length_of( :title, :minimum=>5, :too_short=>"hoo %d" ) + t = Topic.create("title" => "uhoh", "content" => "whatever") + assert !t.valid? + assert t.errors.on(:title) + assert_equal "hoo 5", t.errors["title"] + end + + def test_validates_length_of_custom_errors_for_maximum_with_message + Topic.validates_length_of( :title, :maximum=>5, :message=>"boo %d" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors.on(:title) + assert_equal "boo 5", t.errors["title"] + end + + def test_validates_length_of_custom_errors_for_maximum_with_too_long + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors.on(:title) + assert_equal "hoo 5", t.errors["title"] + end + + def test_validates_length_of_custom_errors_for_is_with_message + Topic.validates_length_of( :title, :is=>5, :message=>"boo %d" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors.on(:title) + assert_equal "boo 5", t.errors["title"] + end + + def test_validates_length_of_custom_errors_for_is_with_wrong_length + Topic.validates_length_of( :title, :is=>5, :wrong_length=>"hoo %d" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors.on(:title) + assert_equal "hoo 5", t.errors["title"] + end + + def kcode_scope(kcode) + orig_kcode = $KCODE + $KCODE = kcode + begin + yield + ensure + $KCODE = orig_kcode + end + end + + def test_validates_length_of_using_minimum_utf8 + kcode_scope('UTF8') do + Topic.validates_length_of :title, :minimum => 5 + + t = Topic.create("title" => "一二三四五", "content" => "whatever") + assert t.valid? + + t.title = "一二三四" + assert !t.valid? + assert t.errors.on(:title) + assert_equal "is too short (minimum is 5 characters)", t.errors["title"] + end + end + + def test_validates_length_of_using_maximum_utf8 + kcode_scope('UTF8') do + Topic.validates_length_of :title, :maximum => 5 + + t = Topic.create("title" => "一二三四五", "content" => "whatever") + assert t.valid? + + t.title = "一二34五六" + assert !t.valid? + assert t.errors.on(:title) + assert_equal "is too long (maximum is 5 characters)", t.errors["title"] + end + end + + def test_validates_length_of_using_within_utf8 + kcode_scope('UTF8') do + Topic.validates_length_of(:title, :content, :within => 3..5) + + t = Topic.new("title" => "一二", "content" => "12三四五六七") + assert !t.valid? + assert_equal "is too short (minimum is 3 characters)", t.errors.on(:title) + assert_equal "is too long (maximum is 5 characters)", t.errors.on(:content) + t.title = "一二三" + t.content = "12三" + assert t.valid? + end + end + + def test_optionally_validates_length_of_using_within_utf8 + kcode_scope('UTF8') do + Topic.validates_length_of :title, :content, :within => 3..5, :allow_nil => true + + t = Topic.create('title' => '一二三', 'content' => '一二三四五') + assert t.valid? + + t.title = nil + assert t.valid? + end + end + + def test_optionally_validates_length_of_using_within_on_create_utf8 + kcode_scope('UTF8') do + Topic.validates_length_of :title, :content, :within => 5..10, :on => :create, :too_long => "é•·ã™ãŽã¾ã™: %d" + + t = Topic.create("title" => "一二三四五六七八ä¹åA", "content" => "whatever") + assert !t.save + assert t.errors.on(:title) + assert_equal "é•·ã™ãŽã¾ã™: 10", t.errors[:title] + + t.title = "一二三四五六七八ä¹" + assert t.save + + t.title = "一二3" + assert t.save + + t.content = "一二三四五六七八ä¹å" + assert t.save + + t.content = t.title = "一二三四五六" + assert t.save + end + end + + def test_optionally_validates_length_of_using_within_on_update_utf8 + kcode_scope('UTF8') do + Topic.validates_length_of :title, :content, :within => 5..10, :on => :update, :too_short => "短ã™ãŽã¾ã™: %d" + + t = Topic.create("title" => "一二三4", "content" => "whatever") + assert !t.save + assert t.errors.on(:title) + + t.title = "1二三4" + assert !t.save + assert t.errors.on(:title) + assert_equal "短ã™ãŽã¾ã™: 5", t.errors[:title] + + t.title = "valid" + t.content = "一二三四五六七八ä¹åA" + assert !t.save + assert t.errors.on(:content) + + t.content = "一二345" + assert t.save + end + end + + def test_validates_length_of_using_is_utf8 + kcode_scope('UTF8') do + Topic.validates_length_of :title, :is => 5 + + t = Topic.create("title" => "一二345", "content" => "whatever") + assert t.valid? + + t.title = "一二345å…­" + assert !t.valid? + assert t.errors.on(:title) + assert_equal "is the wrong length (should be 5 characters)", t.errors["title"] + end + end + + def test_validates_size_of_association_utf8 + kcode_scope('UTF8') do + assert_nothing_raised { Topic.validates_size_of :replies, :minimum => 1 } + t = Topic.new('title' => 'ã‚ã„ã†ãˆãŠ', 'content' => 'ã‹ããã‘ã“') + assert !t.save + assert t.errors.on(:replies) + t.replies.create('title' => 'ã‚ã„ã†ãˆãŠ', 'content' => 'ã‹ããã‘ã“') + assert t.valid? + end + end + + def test_validates_associated_many + Topic.validates_associated( :replies ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + t.replies << [r = Reply.create("title" => "A reply"), r2 = Reply.create("title" => "Another reply")] + assert !t.valid? + assert t.errors.on(:replies) + assert_equal 1, r.errors.count # make sure all associated objects have been validated + assert_equal 1, r2.errors.count + r.content = r2.content = "non-empty" + assert t.valid? + end + + def test_validates_associated_one + Reply.validates_associated( :topic ) + Topic.validates_presence_of( :content ) + r = Reply.create("title" => "A reply", "content" => "with content!") + r.topic = Topic.create("title" => "uhohuhoh") + assert !r.valid? + assert r.errors.on(:topic) + r.topic.content = "non-empty" + assert r.valid? + end + + def test_validate_block + Topic.validate { |topic| topic.errors.add("title", "will never be valid") } + t = Topic.create("title" => "Title", "content" => "whatever") + assert !t.valid? + assert t.errors.on(:title) + assert_equal "will never be valid", t.errors["title"] + end + + def test_invalid_validator + Topic.validate 3 + assert_raise(ActiveRecord::ActiveRecordError) { t = Topic.create } + end + + def test_throw_away_typing + d = Developer.create "name" => "David", "salary" => "100,000" + assert !d.valid? + assert_equal 100, d.salary + assert_equal "100,000", d.salary_before_type_cast + end + + def test_validates_acceptance_of_with_custom_error_using_quotes + Developer.validates_acceptance_of :salary, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.salary = "0" + assert !d.valid? + assert_equal d.errors.on(:salary).first, "This string contains 'single' and \"double\" quotes" + end + + def test_validates_confirmation_of_with_custom_error_using_quotes + Developer.validates_confirmation_of :name, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "John" + d.name_confirmation = "Johnny" + assert !d.valid? + assert_equal d.errors.on(:name), "This string contains 'single' and \"double\" quotes" + end + + def test_validates_format_of_with_custom_error_using_quotes + Developer.validates_format_of :name, :with => /^(A-Z*)$/, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "John 32" + assert !d.valid? + assert_equal d.errors.on(:name), "This string contains 'single' and \"double\" quotes" + end + + def test_validates_inclusion_of_with_custom_error_using_quotes + Developer.validates_inclusion_of :salary, :in => 1000..80000, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.salary = "90,000" + assert !d.valid? + assert_equal d.errors.on(:salary).first, "This string contains 'single' and \"double\" quotes" + end + + def test_validates_length_of_with_custom_too_long_using_quotes + Developer.validates_length_of :name, :maximum => 4, :too_long=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "Jeffrey" + assert !d.valid? + assert_equal d.errors.on(:name).first, "This string contains 'single' and \"double\" quotes" + end + + def test_validates_length_of_with_custom_too_short_using_quotes + Developer.validates_length_of :name, :minimum => 4, :too_short=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "Joe" + assert !d.valid? + assert_equal d.errors.on(:name).first, "This string contains 'single' and \"double\" quotes" + end + + def test_validates_length_of_with_custom_message_using_quotes + Developer.validates_length_of :name, :minimum => 4, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "Joe" + assert !d.valid? + assert_equal d.errors.on(:name).first, "This string contains 'single' and \"double\" quotes" + end + + def test_validates_presence_of_with_custom_message_using_quotes + Developer.validates_presence_of :non_existent, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "Joe" + assert !d.valid? + assert_equal d.errors.on(:non_existent), "This string contains 'single' and \"double\" quotes" + end + + def test_validates_uniqueness_of_with_custom_message_using_quotes + Developer.validates_uniqueness_of :name, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "David" + assert !d.valid? + assert_equal d.errors.on(:name).first, "This string contains 'single' and \"double\" quotes" + end + + def test_validates_associated_with_custom_message_using_quotes + Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes" + Topic.validates_presence_of :content + r = Reply.create("title" => "A reply", "content" => "with content!") + r.topic = Topic.create("title" => "uhohuhoh") + assert !r.valid? + assert_equal r.errors.on(:topic).first, "This string contains 'single' and \"double\" quotes" + end + + def test_conditional_validation_using_method_true + # When the method returns true + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => :condition_is_true ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors.on(:title) + assert_equal "hoo 5", t.errors["title"] + end + + def test_conditional_validation_using_method_false + # When the method returns false + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => :condition_is_true_but_its_not ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert t.valid? + assert !t.errors.on(:title) + end + + def test_conditional_validation_using_string_true + # When the evaluated string returns true + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => "a = 1; a == 1" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors.on(:title) + assert_equal "hoo 5", t.errors["title"] + end + + def test_conditional_validation_using_string_false + # When the evaluated string returns false + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => "false") + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert t.valid? + assert !t.errors.on(:title) + end + + def test_conditional_validation_using_block_true + # When the block returns true + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", + :if => Proc.new { |r| r.content.size > 4 } ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors.on(:title) + assert_equal "hoo 5", t.errors["title"] + end + + def test_conditional_validation_using_block_false + # When the block returns false + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", + :if => Proc.new { |r| r.title != "uhohuhoh"} ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert t.valid? + assert !t.errors.on(:title) + end + + def test_validates_associated_missing + Reply.validates_presence_of(:topic) + r = Reply.create("title" => "A reply", "content" => "with content!") + assert !r.valid? + assert r.errors.on(:topic) + + r.topic = Topic.find :first + assert r.valid? + end +end + + +class ValidatesNumericalityTest + NIL = [nil, "", " ", " \t \r \n"] + FLOAT_STRINGS = %w(0.0 +0.0 -0.0 10.0 10.5 -10.5 -0.0001 -090.1) + INTEGER_STRINGS = %w(0 +0 -0 10 +10 -10 0090 -090) + FLOATS = [0.0, 10.0, 10.5, -10.5, -0.0001] + FLOAT_STRINGS + INTEGERS = [0, 10, -10] + INTEGER_STRINGS + JUNK = ["not a number", "42 not a number", "0xdeadbeef", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12"] + + def setup + Topic.write_inheritable_attribute(:validate, nil) + Topic.write_inheritable_attribute(:validate_on_create, nil) + Topic.write_inheritable_attribute(:validate_on_update, nil) + end + + def test_default_validates_numericality_of + Topic.validates_numericality_of :approved + + invalid!(NIL + JUNK) + valid!(FLOATS + INTEGERS) + end + + def test_validates_numericality_of_with_nil_allowed + Topic.validates_numericality_of :approved, :allow_nil => true + + invalid!(JUNK) + valid!(NIL + FLOATS + INTEGERS) + end + + def test_validates_numericality_of_with_integer_only + Topic.validates_numericality_of :approved, :only_integer => true + + invalid!(NIL + JUNK + FLOATS) + valid!(INTEGERS) + end + + def test_validates_numericality_of_with_integer_only_and_nil_allowed + Topic.validates_numericality_of :approved, :only_integer => true, :allow_nil => true + + invalid!(JUNK + FLOATS) + valid!(NIL + INTEGERS) + end + + private + def invalid!(values) + values.each do |value| + topic = Topic.create("title" => "numeric test", "content" => "whatever", "approved" => value) + assert !topic.valid?, "#{value} not rejected as a number" + assert topic.errors.on(:approved) + end + end + + def valid!(values) + values.each do |value| + topic = Topic.create("title" => "numeric test", "content" => "whatever", "approved" => value) + assert topic.valid?, "#{value} not accepted as a number" + end + end +end diff --git a/vendor/rails/activesupport/CHANGELOG b/vendor/rails/activesupport/CHANGELOG new file mode 100644 index 00000000..66c9fe26 --- /dev/null +++ b/vendor/rails/activesupport/CHANGELOG @@ -0,0 +1,456 @@ +*1.3.1* (April 6th, 2005) + +* Clean paths inside of exception messages and traces. [Nicholas Seckar] + +* Add Pathname.clean_within for cleaning all the paths inside of a string. [Nicholas Seckar] + +* provide an empty Dependencies::LoadingModule.load which prints deprecation warnings. Lets 1.0 applications function with .13-style environment.rb. + + +*1.3.0* (March 27th, 2005) + +* When possible, avoid incorrectly obtaining constants from parent modules. Fixes #4221. [Nicholas Seckar] + +* Add more tests for dependencies; refactor existing cases. [Nicholas Seckar] + +* Move Module#parent and Module#as_load_path into core_ext. Add Module#parent. [Nicholas Seckar] + +* Add CachingTools::HashCaching to simplify the creation of nested, autofilling hashes. [Nicholas Seckar] + +* Remove a hack intended to avoid unloading the same class twice, but which would not work anyways. [Nicholas Seckar] + +* Update Object.subclasses_of to locate nested classes. This affects Object.remove_subclasses_of in that nested classes will now be unloaded. [Nicholas Seckar] + +* Update Object.remove_subclasses_of to use Class.remove_class, reducing duplication. [Nicholas Seckar] + +* Added Fixnum#seconds for consistency, so you can say 5.minutes + 30.seconds instead of 5.minutes + 30 #4389 [François Beausoleil] + +* Added option to String#camelize to generate lower-cased camel case by passing in :lower, like "super_man".camelize(:lower) # => "superMan" [DHH] + +* Added Hash#diff to show the difference between two hashes [Chris McGrath] + +* Added Time#advance to do precise time time calculations for cases where a month being approximated to 30 days won't do #1860 [Rick Olson] + +* Enhance Inflector.underscore to convert '-' into '_' (as the inverse of Inflector.dasherize) [Jamis Buck] + +* Switched to_xml to use the xml schema format for datetimes. This allows the encoding of time zones and should improve operability. [Koz] + +* Added a note to the documentation for the Date related Numeric extensions to indicate that they're +approximations and shouldn't be used for critical calculations. [Koz] + +* Added Hash#to_xml and Array#to_xml that makes it much easier to produce XML from basic structures [DHH]. Examples: + + { :name => "David", :street_name => "Paulina", :age => 26, :moved_on => Date.new(2005, 11, 15) }.to_xml + + ...returns: + + + Paulina + David + 26 + 2005-11-15 + + +* Moved Jim Weirich's wonderful Builder from Action Pack to Active Support (it's simply too useful to be stuck in AP) [DHH] + +* Fixed that Array#to_sentence will return "" on an empty array instead of ", and" #3842, #4031 [rubyonrails@beautifulpixel.com] + +* Add Enumerable#group_by for grouping collections based on the result of some + block. Useful, for example, for grouping records by date. + + ex. + + latest_transcripts.group_by(&:day).each do |day, transcripts| + p "#{day} -> #{transcripts.map(&:class) * ', '}" + end + "2006-03-01 -> Transcript" + "2006-02-28 -> Transcript" + "2006-02-27 -> Transcript, Transcript" + "2006-02-26 -> Transcript, Transcript" + + Add Array#in_groups_of, for iterating over an array in groups of a certain + size. + + ex. + + %w(1 2 3 4 5 6 7).in_groups_of(3) {|g| p g} + ["1", "2", "3"] + ["4", "5", "6"] + ["7", nil, nil] + + [Marcel Molina Jr., Sam Stephenson] + +* Added Kernel#daemonize to turn the current process into a daemon that can be killed with a TERM signal [DHH] + +* Add 'around' methods to Logger, to make it easy to log before and after messages for a given block as requested in #3809. [Michael Koziarski] Example: + + logger.around_info("Start rendering component (#{options.inspect}): ", + "\n\nEnd of component rendering") { yield } + +* Added Time#beginning_of_quarter #3607 [cohen.jeff@gmail.com] + +* Fix Object.subclasses_of to only return currently defined objects [Jonathan Viney ] + +* Fix constantize to properly handle names beginning with '::'. [Nicholas Seckar] + +* Make String#last return the string instead of nil when it is shorter than the limit [Scott Barron]. + +* Added delegation support to Module that allows multiple delegations at once (unlike Forwardable in the stdlib) [DHH]. Example: + + class Account < ActiveRecord::Base + has_one :subscription + delegate :free?, :paying?, :to => :subscription + delegate :overdue?, :to => "subscription.last_payment" + end + + account.free? # => account.subscription.free? + account.overdue? # => account.subscription.last_payment.overdue? + +* Fix Reloadable to handle the case where a class that has been 'removed' has not yet been garbage collected. [Nicholas Seckar] + +* Don't allow Reloadable to be included into Modules. + +* Remove LoadingModule. [Nicholas Seckar] + +* Add documentation for Reloadable::Subclasses. [Nicholas Seckar] + +* Add Reloadable::Subclasses which handles the common case where a base class should not be reloaded, but its subclasses should be. [Nicholas Seckar] + +* Further improvements to reloading code [Nicholas Seckar, Trevor Squires] + + - All classes/modules which include Reloadable can define reloadable? for fine grained control of reloading + - Class.remove_class uses Module#parent to access the parent module + - Class.remove_class expanded to handle multiple classes in a single call + - LoadingModule.clear! has been removed as it is no longer required + - Module#remove_classes_including has been removed in favor of Reloadable.reloadable_classes + +* Added reusable reloading support through the inclusion of the Relodable module that all subclasses of ActiveRecord::Base, ActiveRecord::Observer, ActiveController::Base, and ActionMailer::Base automatically gets. This means that these classes will be reloaded by the dispatcher when Dependencies.mechanism = :load. You can make your own models reloadable easily: + + class Setting + include Reloadable + end + + Reloading a class is done by removing its constant which will cause it to be loaded again on the next reference. [DHH] + +* Added auto-loading support for classes in modules, so Conductor::Migration will look for conductor/migration.rb and Conductor::Database::Settings will look for conductor/database/settings.rb [Nicholas Seckar] + +* Add Object#instance_exec, like instance_eval but passes its arguments to the block. (Active Support will not override the Ruby 1.9 implementation of this method.) [Sam Stephenson] + +* Add Proc#bind(object) for changing a proc or block's self by returning a Method bound to the given object. Based on why the lucky stiff's "cloaker" method. [Sam Stephenson] + +* Fix merge and dup for hashes with indifferent access #3404 [kenneth.miller@bitfield.net] + +* Fix the requires in option_merger_test to unbreak AS tests. [Sam Stephenson] + +* Make HashWithIndifferentAccess#update behave like Hash#update by returning the hash. #3419, #3425 [asnem@student.ethz.ch, JanPrill@blauton.de, Marcel Molina Jr.] + +* Add ActiveSupport::JSON and Object#to_json for converting Ruby objects to JSON strings. [Sam Stephenson] + +* Add Object#with_options for DRYing up multiple calls to methods having shared options. [Sam Stephenson] Example: + + ActionController::Routing::Routes.draw do |map| + # Account routes + map.with_options(:controller => 'account') do |account| + account.home '', :action => 'dashboard' + account.signup 'signup', :action => 'new' + account.logout 'logout', :action => 'logout' + end + end + +* Introduce Dependencies.warnings_on_first_load setting. If true, enables warnings on first load of a require_dependency. Otherwise, loads without warnings. Disabled (set to false) by default. [Jeremy Kemper] + +* Active Support is warnings-safe. #1792 [Eric Hodel] + +* Introduce enable_warnings counterpart to silence_warnings. Turn warnings on when loading a file for the first time if Dependencies.mechanism == :load. Common mistakes such as redefined methods will print warnings to stderr. [Jeremy Kemper] + +* Add Symbol#to_proc, which allows for, e.g. [:foo, :bar].map(&:to_s). [Marcel Molina Jr.] + +* Added the following methods [Marcel Molina Jr., Sam Stephenson]: + * Object#copy_instance_variables_from(object) to copy instance variables from one object to another + * Object#extended_by to get an instance's included/extended modules + * Object#extend_with_included_modules_from(object) to extend an instance with the modules from another instance + +*1.2.5* (December 13th, 2005) + +* Become part of Rails 1.0 + +* Rename Version constant to VERSION. #2802 [Marcel Molina Jr.] + +*1.2.3* (November 7th, 2005) + +* Change Inflector#constantize to use eval instead of const_get. [Nicholas Seckar] + +* Fix const_missing handler to ignore the trailing '.rb' on files when comparing paths. [Nicholas Seckar] + +* Define kernel.rb methods in "class Object" instead of "module Kernel" to work around a Windows peculiarity [Sam Stephenson] + +* Fix broken tests caused by incomplete loading of active support. [Nicholas Seckar] + +* Fix status pluralization bug so status_codes doesn't get pluralized as statuses_code. #2758 [keithm@infused.org] + +* Added Kernel#silence_stderr to silence stderr for the duration of the given block [Sam Stephenson] + +* Changed Kernel#` to print a message to stderr (like Unix) instead of raising Errno::ENOENT on Win32 [Sam Stephenson] + +* Changed 0.blank? to false rather than true since it violates everyone's expectation of blankness. #2518, #2705 [rails@jeffcole.net] + +* When loading classes using const_missing, raise a NameError if and only if the file we tried to load was not present. [Nicholas Seckar] + +* Added petabytes and exebytes to numeric extensions #2397 [timct@mac.com] + +* Added Time#end_of_month to accompany Time#beginning_of_month #2514 [Jens-Christian Fischer] + + +*1.2.2* (October 26th, 2005) + +* Set Logger.silencer = false to disable Logger#silence. Useful for debugging fixtures. + +* Add title case method to String to do, e.g., 'action_web_service'.titlecase # => 'Action Web Service'. [Marcel Molina Jr.] + + +*1.2.1* (October 19th, 2005) + +* Classify generated routing code as framework code to avoid appearing in application traces. [Nicholas Seckar] + +* Show all framework frames in the framework trace. [Nicholas Seckar] + + +*1.2.0* (October 16th, 2005) + +* Update Exception extension to show the first few framework frames in an application trace. [Nicholas Seckar] + +* Added Exception extension to provide support for clean backtraces. [Nicholas Seckar] + +* Updated whiny nil to be more concise and useful. [Nicholas Seckar] + +* Added Enumerable#first_match [Nicholas Seckar] + +* Fixed that Time#change should also reset usec when also resetting minutes #2459 [ikeda@dream.big.or.jp] + +* Fix Logger compatibility for distributions that don't keep Ruby and its standard library in sync. + +* Replace '%e' from long and short time formats as Windows does not support it. #2344. [Tom Ward ] + +* Added to_s(:db) to Range, so you can get "BETWEEN '2005-12-10' AND '2005-12-12'" from Date.new(2005, 12, 10)..Date.new(2005, 12, 12) (and likewise with Times) + +* Moved require_library_or_gem into Kernel. #1992 [Michael Schuerig ] + +* Add :rfc822 as an option for Time#to_s (to get rfc822-formatted times) + +* Chain the const_missing hook to any previously existing hook so rails can play nicely with rake + +* Clean logger is compatible with both 1.8.2 and 1.8.3 Logger. #2263 [Michael Schuerig ] + +* Added native, faster implementations of .blank? for the core types #2286 [skae] + +* Fixed clean logger to work with Ruby 1.8.3 Logger class #2245 + +* Fixed memory leak with Active Record classes when Dependencies.mechanism = :load #1704 [c.r.mcgrath@gmail.com] + +* Fixed Inflector.underscore for use with acronyms, so HTML becomes html instead of htm_l #2173 [k@v2studio.com] + +* Fixed dependencies related infinite recursion bug when a controller file does not contain a controller class. Closes #1760. [rcolli2@tampabay.rr.com] + +* Fixed inflections for status, quiz, move #2056 [deirdre@deirdre.net] + +* Added Hash#reverse_merge, Hash#reverse_merge!, and Hash#reverse_update to ease the use of default options + +* Added Array#to_sentence that'll turn ['one', 'two', 'three'] into "one, two, and three" #2157 [m.stienstra@fngtps.com] + +* Added Kernel#silence_warnings to turn off warnings temporarily for the passed block + +* Added String#starts_with? and String#ends_with? #2118 [thijs@vandervossen.net] + +* Added easy extendability to the inflector through Inflector.inflections (using the Inflector::Inflections singleton class). Examples: + + Inflector.inflections do |inflect| + inflect.plural /^(ox)$/i, '\1\2en' + inflect.singular /^(ox)en/i, '\1' + + inflect.irregular 'octopus', 'octopi' + + inflect.uncountable "equipment" + end + +* Added String#at, String#from, String#to, String#first, String#last in ActiveSupport::CoreExtensions::String::Access to ease access to individual characters and substrings in a string serving basically as human names for range access. + +* Make Time#last_month work when invoked on the 31st of a month. + +* Add Time.days_in_month, and make Time#next_month work when invoked on the 31st of a month + +* Fixed that Time#midnight would have a non-zero usec on some platforms #1836 + +* Fixed inflections of "index/indices" #1766 [damn_pepe@gmail.com] + +* Added stripping of _id to String#humanize, so "employee_id" becomes "Employee" #1574 [Justin French] + +* Factor Fixnum and Bignum extensions into Integer extensions [Nicholas Seckar] + +* Hooked #ordinalize into Fixnum and Bignum classes. [Nicholas Seckar, danp] + +* Added Fixnum#ordinalize to turn 1.ordinalize to "1st", 3.ordinalize to "3rd", and 10.ordinalize to "10th" and so on #1724 [paul@cnt.org] + + +*1.1.1* (11 July, 2005) + +* Added more efficient implementation of the development mode reset of classes #1638 [Chris McGrath] + + +*1.1.0* (6 July, 2005) + +* Fixed conflict with Glue gem #1606 [Rick Olson] + +* Added new rules to the Inflector to deal with more unusual plurals mouse/louse => mice/lice, information => information, ox => oxen, virus => viri, archive => archives #1571, #1583, #1490, #1599, #1608 [foamdino@gmail.com/others] + +* Fixed memory leak with Object#remove_subclasses_of, which inflicted a Rails application running in development mode with a ~20KB leak per request #1289 [c.r.mcgrath@gmail.com] + +* Made 1.year == 365.25.days to account for leap years. This allows you to do User.find(:all, :conditions => ['birthday > ?', 50.years.ago]) without losing a lot of days. #1488 [tuxie@dekadance.se] + +* Added an exception if calling id on nil to WhinyNil #584 [kevin-temp@writesoon.com] + +* Added Fix/Bignum#multiple_of? which returns true on 14.multiple_of?(7) and false on 16.multiple_of?(7) #1464 [Thomas Fuchs] + +* Added even? and odd? to work with Bignums in addition to Fixnums #1464 [Thomas Fuchs] + +* Fixed Time#at_beginning_of_week returned the next Monday instead of the previous one when called on a Sunday #1403 [jean.helou@gmail.com] + +* Increased the speed of indifferent hash access by using Hash#default. #1436 [Nicholas Seckar] + +* Added that " " is now also blank? (using strip if available) + +* Fixed Dependencies so all modules are able to load missing constants #1173 [Nicholas Seckar] + +* Fixed the Inflector to underscore strings containing numbers, so Area51Controller becomes area51_controller #1176 [Nicholas Seckar] + +* Fixed that HashWithIndifferentAccess stringified all keys including symbols, ints, objects, and arrays #1162 [Nicholas Seckar] + +* Fixed Time#last_year to go back in time, not forward #1278 [fabien@odilat.com] + +* Fixed the pluralization of analysis to analyses #1295 [seattle@rootimage.msu.edu] + +* Fixed that Time.local(2005,12).months_since(1) would raise "ArgumentError: argument out of range" #1311 [jhahn@niveon.com] + +* Added silencing to the default Logger class + + +*1.0.4* (19th April, 2005) + +* Fixed that in some circumstances controllers outside of modules may have hidden ones inside modules. For example, admin/content might have been hidden by /content. #1075 [Nicholas Seckar] + +* Fixed inflection of perspectives and similar words #1045 [thijs@vandervossen.net] + +* Added Fixnum#even? and Fixnum#odd? + +* Fixed problem with classes being required twice. Object#const_missing now uses require_dependency to load files. It used to use require_or_load which would cause models to be loaded twice, which was not good for validations and other class methods #971 [Nicholas Seckar] + + +*1.0.3* (27th March, 2005) + +* Fixed Inflector.pluralize to handle capitalized words #932 [Jeremy Kemper] + +* Added Object#suppress which allows you to make a saner choice around with exceptions to swallow #980. Example: + + suppress(ZeroDivisionError) { 1/0 } + + ...instead of: + + 1/0 rescue nil # BAD, EVIL, DIRTY. + + +*1.0.2* (22th March, 2005) + +* Added Kernel#returning -- a Ruby-ized realization of the K combinator, courtesy of Mikael Brockman. + + def foo + returning values = [] do + values << 'bar' + values << 'baz' + end + end + + foo # => ['bar', 'baz'] + + +*1.0.1* (7th March, 2005) + +* Fixed Hash#indifferent_access to also deal with include? and fetch and nested hashes #726 [Nicholas Seckar] + +* Added Object#blank? -- see http://redhanded.hobix.com/inspect/objectBlank.html #783 [_why the lucky stiff] + +* Added inflection rules for "sh" words, like "wish" and "fish" #755 [phillip@pjbsoftware.com] + +* Fixed an exception when using Ajax based requests from Safari because Safari appends a \000 to the post body. Symbols can't have \000 in them so indifferent access would throw an exception in the constructor. Indifferent hashes now use strings internally instead. #746 [Tobias Luetke] + +* Added String#to_time and String#to_date for wrapping ParseDate + + +*1.0.0* (24th February, 2005) + +* Added TimeZone as the first of a number of value objects that among others Active Record can use rich value objects using composed_of #688 [Jamis Buck] + +* Added Date::Conversions for getting dates in different convenient string representations and other objects + +* Added Time::Conversions for getting times in different convenient string representations and other objects + +* Added Time::Calculations to ask for things like Time.now.tomorrow, Time.now.yesterday, Time.now.months_ago(4) #580 [DP|Flurin]. Examples: + + "Later today" => now.in(3.hours), + "Tomorrow morning" => now.tomorrow.change(:hour => 9), + "Tomorrow afternoon" => now.tomorrow.change(:hour => 14), + "In a couple of days" => now.tomorrow.tomorrow.change(:hour => 9), + "Next monday" => now.next_week.change(:hour => 9), + "In a month" => now.next_month.change(:hour => 9), + "In 6 months" => now.months_since(6).change(:hour => 9), + "In a year" => now.in(1.year).change(:hour => 9) + +* Upgraded to breakpoint 92 which fixes: + + * overload IRB.parse_opts(), fixes #443 + => breakpoints in tests work even when running them via rake + * untaint handlers, might fix an issue discussed on the Rails ML + * added verbose mode to breakpoint_client + * less noise caused by breakpoint_client by default + * ignored TerminateLineInput exception in signal handler + => quiet exit on Ctrl-C + +* Fixed Inflector for words like "news" and "series" that are the same in plural and singular #603 [echion], #615 [marcenuc] + +* Added Hash#stringify_keys and Hash#stringify_keys! + +* Added IndifferentAccess as a way to wrap a hash by a symbol-based store that also can be accessed by string keys + +* Added Inflector.constantize to turn "Admin::User" into a reference for the constant Admin::User + +* Added that Inflector.camelize and Inflector.underscore can deal with modules like turning "Admin::User" into "admin/user" and back + +* Added Inflector.humanize to turn attribute names like employee_salary into "Employee salary". Used by automated error reporting in AR. + +* Added availability of class inheritable attributes to the masses #477 [Jeremy Kemper] + + class Foo + class_inheritable_reader :read_me + class_inheritable_writer :write_me + class_inheritable_accessor :read_and_write_me + class_inheritable_array :read_and_concat_me + class_inheritable_hash :read_and_update_me + end + + # Bar gets a clone of (not a reference to) Foo's attributes. + class Bar < Foo + end + + Bar.read_and_write_me == Foo.read_and_write_me + Bar.read_and_write_me = 'bar' + Bar.read_and_write_me != Foo.read_and_write_me + +* Added Inflections as an extension on String, so Inflector.pluralize(Inflector.classify(name)) becomes name.classify.pluralize #476 [Jeremy Kemper] + +* Added Byte operations to Numeric, so 5.5.megabytes + 200.kilobytes #461 [Marcel Molina] + +* Fixed that Dependencies.reload can't load the same file twice #420 [Kent Sibilev] + +* Added Fixnum#ago/until, Fixnum#since/from_now #450 [Jeremy Kemper] + +* Added that Inflector now accepts Symbols and Classes by calling .to_s on the word supplied + +* Added time unit extensions to Fixnum that'll return the period in seconds, like 2.days + 4.hours. diff --git a/vendor/rails/activesupport/README b/vendor/rails/activesupport/README new file mode 100644 index 00000000..9fb9a80c --- /dev/null +++ b/vendor/rails/activesupport/README @@ -0,0 +1,43 @@ += Active Support -- Utility classes and standard library extensions from Rails + +Active Support is a collection of various utility classes and standard library extensions that were found useful +for Rails. All these additions have hence been collected in this bundle as way to gather all that sugar that makes +Ruby sweeter. + + +== Download + +The latest version of Active Support can be found at + +* http://rubyforge.org/project/showfiles.php?group_id=182 + +Documentation can be found at + +* http://as.rubyonrails.com + + +== Installation + +The preferred method of installing Active Support is through its GEM file. You'll need to have +RubyGems[http://rubygems.rubyforge.org/wiki/wiki.pl] installed for that, though. If you have it, +then use: + + % [sudo] gem install activesupport-1.0.0.gem + + +== License + +Active Support is released under the MIT license. + + +== Support + +The Active Support homepage is http://www.rubyonrails.com. You can find the Active Support +RubyForge page at http://rubyforge.org/projects/activesupport. And as Jim from Rake says: + + Feel free to submit commits or feature requests. If you send a patch, + remember to update the corresponding unit tests. If fact, I prefer + new feature to be submitted in the form of new unit tests. + +For other information, feel free to ask on the ruby-talk mailing list +(which is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com. diff --git a/vendor/rails/activesupport/Rakefile b/vendor/rails/activesupport/Rakefile new file mode 100644 index 00000000..a7f0db56 --- /dev/null +++ b/vendor/rails/activesupport/Rakefile @@ -0,0 +1,82 @@ +require 'rake/testtask' +require 'rake/rdoctask' +require 'rake/gempackagetask' +require 'rake/contrib/rubyforgepublisher' +require File.join(File.dirname(__FILE__), 'lib', 'active_support', 'version') + +PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' +PKG_NAME = 'activesupport' +PKG_VERSION = ActiveSupport::VERSION::STRING + PKG_BUILD +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" + +RELEASE_NAME = "REL #{PKG_VERSION}" + +RUBY_FORGE_PROJECT = "activesupport" +RUBY_FORGE_USER = "webster132" + +task :default => :test +Rake::TestTask.new { |t| + t.pattern = 'test/**/*_test.rb' + t.verbose = true + t.warning = false +} + +# Create compressed packages +dist_dirs = [ "lib", "test"] + +# Genereate the RDoc documentation + +Rake::RDocTask.new { |rdoc| + rdoc.rdoc_dir = 'doc' + rdoc.title = "Active Support -- Utility classes and standard library extensions from Rails" + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.template = "#{ENV['template']}.rb" if ENV['template'] + rdoc.rdoc_files.include('README', 'CHANGELOG') + rdoc.rdoc_files.include('lib/active_support.rb') + rdoc.rdoc_files.include('lib/active_support/*.rb') + rdoc.rdoc_files.include('lib/active_support/**/*.rb') +} + +spec = Gem::Specification.new do |s| + s.name = PKG_NAME + s.version = PKG_VERSION + s.summary = "Support and utility classes used by the Rails framework." + s.description = %q{Utility library which carries commonly used classes and goodies from the Rails framework} + + s.files = [ "CHANGELOG" ] + Dir.glob( "lib/**/*" ).delete_if { |item| item.include?( "\.svn" ) } + s.require_path = 'lib' + s.has_rdoc = true + + s.author = "David Heinemeier Hansson" + s.email = "david@loudthinking.com" + s.homepage = "http://www.rubyonrails.org" + s.rubyforge_project = "activesupport" +end + +Rake::GemPackageTask.new(spec) do |p| + p.gem_spec = spec + p.need_tar = true + p.need_zip = true +end + +desc "Publish the beta gem" +task :pgem => [:package] do + Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload + `ssh davidhh@wrath.rubyonrails.org './gemupdate.sh'` +end + +desc "Publish the API documentation" +task :pdoc => [:rdoc] do + Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/as", "doc").upload +end + +desc "Publish the release files to RubyForge." +task :release => [ :package ] do + `rubyforge login` + + for ext in %w( gem tgz zip ) + release_command = "rubyforge add_release #{PKG_NAME} #{PKG_NAME} 'REL #{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}" + puts release_command + system(release_command) + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/install.rb b/vendor/rails/activesupport/install.rb new file mode 100644 index 00000000..f84f06bd --- /dev/null +++ b/vendor/rails/activesupport/install.rb @@ -0,0 +1,30 @@ +require 'rbconfig' +require 'find' +require 'ftools' + +include Config + +# this was adapted from rdoc's install.rb by ways of Log4r + +$sitedir = CONFIG["sitelibdir"] +unless $sitedir + version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"] + $libdir = File.join(CONFIG["libdir"], "ruby", version) + $sitedir = $:.find {|x| x =~ /site_ruby/ } + if !$sitedir + $sitedir = File.join($libdir, "site_ruby") + elsif $sitedir !~ Regexp.quote(version) + $sitedir = File.join($sitedir, version) + end +end + +# the acual gruntwork +Dir.chdir("lib") + +Find.find("active_support", "active_support.rb") { |f| + if f[-3..-1] == ".rb" + File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true) + else + File::makedirs(File.join($sitedir, *f.split(/\//))) + end +} diff --git a/vendor/rails/activesupport/lib/active_support.rb b/vendor/rails/activesupport/lib/active_support.rb new file mode 100644 index 00000000..037905be --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support.rb @@ -0,0 +1,41 @@ +#-- +# Copyright (c) 2005 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +$:.unshift(File.dirname(__FILE__)) +$:.unshift(File.dirname(__FILE__) + "/active_support/vendor") + +require 'builder' + +require 'active_support/inflector' + +require 'active_support/core_ext' +require 'active_support/clean_logger' +require 'active_support/dependencies' +require 'active_support/reloadable' + +require 'active_support/ordered_options' +require 'active_support/option_merger' + +require 'active_support/values/time_zone' + +require 'active_support/json' \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/binding_of_caller.rb b/vendor/rails/activesupport/lib/active_support/binding_of_caller.rb new file mode 100644 index 00000000..e224c996 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/binding_of_caller.rb @@ -0,0 +1,84 @@ +begin + require 'simplecc' +rescue LoadError + class Continuation # :nodoc: # for RDoc + end + def Continuation.create(*args, &block) # :nodoc: + cc = nil; result = callcc {|c| cc = c; block.call(cc) if block and args.empty?} + result ||= args + return *[cc, *result] + end +end + +class Binding; end # for RDoc +# This method returns the binding of the method that called your +# method. It will raise an Exception when you're not inside a method. +# +# It's used like this: +# def inc_counter(amount = 1) +# Binding.of_caller do |binding| +# # Create a lambda that will increase the variable 'counter' +# # in the caller of this method when called. +# inc = eval("lambda { |arg| counter += arg }", binding) +# # We can refer to amount from inside this block safely. +# inc.call(amount) +# end +# # No other statements can go here. Put them inside the block. +# end +# counter = 0 +# 2.times { inc_counter } +# counter # => 2 +# +# Binding.of_caller must be the last statement in the method. +# This means that you will have to put everything you want to +# do after the call to Binding.of_caller into the block of it. +# This should be no problem however, because Ruby has closures. +# If you don't do this an Exception will be raised. Because of +# the way that Binding.of_caller is implemented it has to be +# done this way. +def Binding.of_caller(&block) + old_critical = Thread.critical + Thread.critical = true + count = 0 + cc, result, error, extra_data = Continuation.create(nil, nil) + error.call if error + + tracer = lambda do |*args| + type, context, extra_data = args[0], args[4], args + if type == "return" + count += 1 + # First this method and then calling one will return -- + # the trace event of the second event gets the context + # of the method which called the method that called this + # method. + if count == 2 + # It would be nice if we could restore the trace_func + # that was set before we swapped in our own one, but + # this is impossible without overloading set_trace_func + # in current Ruby. + set_trace_func(nil) + cc.call(eval("binding", context), nil, extra_data) + end + elsif type == "line" then + nil + elsif type == "c-return" and extra_data[3] == :set_trace_func then + nil + else + set_trace_func(nil) + error_msg = "Binding.of_caller used in non-method context or " + + "trailing statements of method using it aren't in the block." + cc.call(nil, lambda { raise(ArgumentError, error_msg) }, nil) + end + end + + unless result + set_trace_func(tracer) + return nil + else + Thread.critical = old_critical + case block.arity + when 1 then yield(result) + else yield(result, extra_data) + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/breakpoint.rb b/vendor/rails/activesupport/lib/active_support/breakpoint.rb new file mode 100755 index 00000000..785dac75 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/breakpoint.rb @@ -0,0 +1,523 @@ +# The Breakpoint library provides the convenience of +# being able to inspect and modify state, diagnose +# bugs all via IRB by simply setting breakpoints in +# your applications by the call of a method. +# +# This library was written and is supported by me, +# Florian Gross. I can be reached at flgr@ccan.de +# and enjoy getting feedback about my libraries. +# +# The whole library (including breakpoint_client.rb +# and binding_of_caller.rb) is licensed under the +# same license that Ruby uses. (Which is currently +# either the GNU General Public License or a custom +# one that allows for commercial usage.) If you for +# some good reason need to use this under another +# license please contact me. + +require 'irb' +require File.dirname(__FILE__) + '/binding_of_caller' unless defined? Binding.of_caller +require 'drb' +require 'drb/acl' + +module Breakpoint + id = %q$Id: breakpoint.rb 92 2005-02-04 22:35:53Z flgr $ + Version = id.split(" ")[2].to_i + + extend self + + # This will pop up an interactive ruby session at a + # pre-defined break point in a Ruby application. In + # this session you can examine the environment of + # the break point. + # + # You can get a list of variables in the context using + # local_variables via +local_variables+. You can then + # examine their values by typing their names. + # + # You can have a look at the call stack via +caller+. + # + # The source code around the location where the breakpoint + # was executed can be examined via +source_lines+. Its + # argument specifies how much lines of context to display. + # The default amount of context is 5 lines. Note that + # the call to +source_lines+ can raise an exception when + # it isn't able to read in the source code. + # + # breakpoints can also return a value. They will execute + # a supplied block for getting a default return value. + # A custom value can be returned from the session by doing + # +throw(:debug_return, value)+. + # + # You can also give names to break points which will be + # used in the message that is displayed upon execution + # of them. + # + # Here's a sample of how breakpoints should be placed: + # + # class Person + # def initialize(name, age) + # @name, @age = name, age + # breakpoint("Person#initialize") + # end + # + # attr_reader :age + # def name + # breakpoint("Person#name") { @name } + # end + # end + # + # person = Person.new("Random Person", 23) + # puts "Name: #{person.name}" + # + # And here is a sample debug session: + # + # Executing break point "Person#initialize" at file.rb:4 in `initialize' + # irb(#):001:0> local_variables + # => ["name", "age", "_", "__"] + # irb(#):002:0> [name, age] + # => ["Random Person", 23] + # irb(#):003:0> [@name, @age] + # => ["Random Person", 23] + # irb(#):004:0> self + # => # + # irb(#):005:0> @age += 1; self + # => # + # irb(#):006:0> exit + # Executing break point "Person#name" at file.rb:9 in `name' + # irb(#):001:0> throw(:debug_return, "Overriden name") + # Name: Overriden name + # + # Breakpoint sessions will automatically have a few + # convenience methods available. See Breakpoint::CommandBundle + # for a list of them. + # + # Breakpoints can also be used remotely over sockets. + # This is implemented by running part of the IRB session + # in the application and part of it in a special client. + # You have to call Breakpoint.activate_drb to enable + # support for remote breakpoints and then run + # breakpoint_client.rb which is distributed with this + # library. See the documentation of Breakpoint.activate_drb + # for details. + def breakpoint(id = nil, context = nil, &block) + callstack = caller + callstack.slice!(0, 3) if callstack.first["breakpoint"] + file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures + + message = "Executing break point " + (id ? "#{id.inspect} " : "") + + "at #{file}:#{line}" + (method ? " in `#{method}'" : "") + + if context then + return handle_breakpoint(context, message, file, line, &block) + end + + Binding.of_caller do |binding_context| + handle_breakpoint(binding_context, message, file, line, &block) + end + end + + module CommandBundle #:nodoc: + # Proxy to a Breakpoint client. Lets you directly execute code + # in the context of the client. + class Client #:nodoc: + def initialize(eval_handler) # :nodoc: + eval_handler.untaint + @eval_handler = eval_handler + end + + instance_methods.each do |method| + next if method[/^__.+__$/] + undef_method method + end + + # Executes the specified code at the client. + def eval(code) + @eval_handler.call(code) + end + + # Will execute the specified statement at the client. + def method_missing(method, *args, &block) + if args.empty? and not block + result = eval "#{method}" + else + # This is a bit ugly. The alternative would be using an + # eval context instead of an eval handler for executing + # the code at the client. The problem with that approach + # is that we would have to handle special expressions + # like "self", "nil" or constants ourself which is hard. + remote = eval %{ + result = lambda { |block, *args| #{method}(*args, &block) } + def result.call_with_block(*args, &block) + call(block, *args) + end + result + } + remote.call_with_block(*args, &block) + end + + return result + end + end + + # Returns the source code surrounding the location where the + # breakpoint was issued. + def source_lines(context = 5, return_line_numbers = false) + lines = File.readlines(@__bp_file).map { |line| line.chomp } + + break_line = @__bp_line + start_line = [break_line - context, 1].max + end_line = break_line + context + + result = lines[(start_line - 1) .. (end_line - 1)] + + if return_line_numbers then + return [start_line, break_line, result] + else + return result + end + end + + # Lets an object that will forward method calls to the breakpoint + # client. This is useful for outputting longer things at the client + # and so on. You can for example do these things: + # + # client.puts "Hello" # outputs "Hello" at client console + # # outputs "Hello" into the file temp.txt at the client + # client.File.open("temp.txt", "w") { |f| f.puts "Hello" } + def client() + if Breakpoint.use_drb? then + sleep(0.5) until Breakpoint.drb_service.eval_handler + Client.new(Breakpoint.drb_service.eval_handler) + else + Client.new(lambda { |code| eval(code, TOPLEVEL_BINDING) }) + end + end + end + + def handle_breakpoint(context, message, file = "", line = "", &block) # :nodoc: + catch(:debug_return) do |value| + eval(%{ + @__bp_file = #{file.inspect} + @__bp_line = #{line} + extend Breakpoint::CommandBundle + extend DRbUndumped if self + }, context) rescue nil + + if not use_drb? then + puts message + IRB.start(nil, IRB::WorkSpace.new(context)) + else + @drb_service.add_breakpoint(context, message) + end + + block.call if block + end + end + + # These exceptions will be raised on failed asserts + # if Breakpoint.asserts_cause_exceptions is set to + # true. + class FailedAssertError < RuntimeError #:nodoc: + end + + # This asserts that the block evaluates to true. + # If it doesn't evaluate to true a breakpoint will + # automatically be created at that execution point. + # + # You can disable assert checking in production + # code by setting Breakpoint.optimize_asserts to + # true. (It will still be enabled when Ruby is run + # via the -d argument.) + # + # Example: + # person_name = "Foobar" + # assert { not person_name.nil? } + # + # Note: If you want to use this method from an + # unit test, you will have to call it by its full + # name, Breakpoint.assert. + def assert(context = nil, &condition) + return if Breakpoint.optimize_asserts and not $DEBUG + return if yield + + callstack = caller + callstack.slice!(0, 3) if callstack.first["assert"] + file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures + + message = "Assert failed at #{file}:#{line}#{" in `#{method}'" if method}." + + if Breakpoint.asserts_cause_exceptions and not $DEBUG then + raise(Breakpoint::FailedAssertError, message) + end + + message += " Executing implicit breakpoint." + + if context then + return handle_breakpoint(context, message, file, line) + end + + Binding.of_caller do |context| + handle_breakpoint(context, message, file, line) + end + end + + # Whether asserts should be ignored if not in debug mode. + # Debug mode can be enabled by running ruby with the -d + # switch or by setting $DEBUG to true. + attr_accessor :optimize_asserts + self.optimize_asserts = false + + # Whether an Exception should be raised on failed asserts + # in non-$DEBUG code or not. By default this is disabled. + attr_accessor :asserts_cause_exceptions + self.asserts_cause_exceptions = false + @use_drb = false + + attr_reader :drb_service # :nodoc: + + class DRbService # :nodoc: + include DRbUndumped + + def initialize + @handler = @eval_handler = @collision_handler = nil + + IRB.instance_eval { @CONF[:RC] = true } + IRB.run_config + end + + def collision + sleep(0.5) until @collision_handler + + @collision_handler.untaint + + @collision_handler.call + end + + def ping() end + + def add_breakpoint(context, message) + workspace = IRB::WorkSpace.new(context) + workspace.extend(DRbUndumped) + + sleep(0.5) until @handler + + @handler.untaint + @handler.call(workspace, message) + end + + attr_accessor :handler, :eval_handler, :collision_handler + end + + # Will run Breakpoint in DRb mode. This will spawn a server + # that can be attached to via the breakpoint-client command + # whenever a breakpoint is executed. This is useful when you + # are debugging CGI applications or other applications where + # you can't access debug sessions via the standard input and + # output of your application. + # + # You can specify an URI where the DRb server will run at. + # This way you can specify the port the server runs on. The + # default URI is druby://localhost:42531. + # + # Please note that breakpoints will be skipped silently in + # case the DRb server can not spawned. (This can happen if + # the port is already used by another instance of your + # application on CGI or another application.) + # + # Also note that by default this will only allow access + # from localhost. You can however specify a list of + # allowed hosts or nil (to allow access from everywhere). + # But that will still not protect you from somebody + # reading the data as it goes through the net. + # + # A good approach for getting security and remote access + # is setting up an SSH tunnel between the DRb service + # and the client. This is usually done like this: + # + # $ ssh -L20000:127.0.0.1:20000 -R10000:127.0.0.1:10000 example.com + # (This will connect port 20000 at the client side to port + # 20000 at the server side, and port 10000 at the server + # side to port 10000 at the client side.) + # + # After that do this on the server side: (the code being debugged) + # Breakpoint.activate_drb("druby://127.0.0.1:20000", "localhost") + # + # And at the client side: + # ruby breakpoint_client.rb -c druby://127.0.0.1:10000 -s druby://127.0.0.1:20000 + # + # Running through such a SSH proxy will also let you use + # breakpoint.rb in case you are behind a firewall. + # + # Detailed information about running DRb through firewalls is + # available at http://www.rubygarden.org/ruby?DrbTutorial + def activate_drb(uri = nil, allowed_hosts = ['localhost', '127.0.0.1', '::1'], + ignore_collisions = false) + + return false if @use_drb + + uri ||= 'druby://localhost:42531' + + if allowed_hosts then + acl = ["deny", "all"] + + Array(allowed_hosts).each do |host| + acl += ["allow", host] + end + + DRb.install_acl(ACL.new(acl)) + end + + @use_drb = true + @drb_service = DRbService.new + did_collision = false + begin + @service = DRb.start_service(uri, @drb_service) + rescue Errno::EADDRINUSE + if ignore_collisions then + nil + else + # The port is already occupied by another + # Breakpoint service. We will try to tell + # the old service that we want its port. + # It will then forward that request to the + # user and retry. + unless did_collision then + DRbObject.new(nil, uri).collision + did_collision = true + end + sleep(10) + retry + end + end + + return true + end + + # Deactivates a running Breakpoint service. + def deactivate_drb + @service.stop_service unless @service.nil? + @service = nil + @use_drb = false + @drb_service = nil + end + + # Returns true when Breakpoints are used over DRb. + # Breakpoint.activate_drb causes this to be true. + def use_drb? + @use_drb == true + end +end + +module IRB #:nodoc: + class << self; remove_method :start; end + def self.start(ap_path = nil, main_context = nil, workspace = nil) + $0 = File::basename(ap_path, ".rb") if ap_path + + # suppress some warnings about redefined constants + old_verbose, $VERBOSE = $VERBOSE, nil + IRB.setup(ap_path) + $VERBOSE = old_verbose + + if @CONF[:SCRIPT] then + irb = Irb.new(main_context, @CONF[:SCRIPT]) + else + irb = Irb.new(main_context) + end + + if workspace then + irb.context.workspace = workspace + end + + @CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC] + @CONF[:MAIN_CONTEXT] = irb.context + + old_sigint = trap("SIGINT") do + begin + irb.signal_handle + rescue RubyLex::TerminateLineInput + # ignored + end + end + + catch(:IRB_EXIT) do + irb.eval_input + end + ensure + trap("SIGINT", old_sigint) + end + + class << self + alias :old_CurrentContext :CurrentContext + remove_method :CurrentContext + end + def IRB.CurrentContext + if old_CurrentContext.nil? and Breakpoint.use_drb? then + result = Object.new + def result.last_value; end + return result + else + old_CurrentContext + end + end + def IRB.parse_opts() end + + class Context #:nodoc: + alias :old_evaluate :evaluate + def evaluate(line, line_no) + if line.chomp == "exit" then + exit + else + old_evaluate(line, line_no) + end + end + end + + class WorkSpace #:nodoc: + alias :old_evaluate :evaluate + + def evaluate(*args) + if Breakpoint.use_drb? then + result = old_evaluate(*args) + if args[0] != :no_proxy and + not [true, false, nil].include?(result) + then + result.extend(DRbUndumped) rescue nil + end + return result + else + old_evaluate(*args) + end + end + end + + module InputCompletor #:nodoc: + def self.eval(code, context, *more) + # Big hack, this assumes that InputCompletor + # will only call eval() when it wants code + # to be executed in the IRB context. + IRB.conf[:MAIN_CONTEXT].workspace.evaluate(:no_proxy, code, *more) + end + end +end + +module DRb # :nodoc: + class DRbObject #:nodoc: + undef :inspect if method_defined?(:inspect) + undef :clone if method_defined?(:clone) + end +end + +# See Breakpoint.breakpoint +def breakpoint(id = nil, &block) + Binding.of_caller do |context| + Breakpoint.breakpoint(id, context, &block) + end +end + +# See Breakpoint.assert +def assert(&block) + Binding.of_caller do |context| + Breakpoint.assert(context, &block) + end +end diff --git a/vendor/rails/activesupport/lib/active_support/caching_tools.rb b/vendor/rails/activesupport/lib/active_support/caching_tools.rb new file mode 100644 index 00000000..c889c148 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/caching_tools.rb @@ -0,0 +1,62 @@ +module ActiveSupport + module CachingTools #:nodoc: + + # Provide shortcuts to simply the creation of nested default hashes. This + # pattern is useful, common practice, and unsightly when done manually. + module HashCaching + # Dynamically create a nested hash structure used to cache calls to +method_name+ + # The cache method is named +#{method_name}_cache+ unless :as => :alternate_name + # is given. + # + # The hash structure is created using nested Hash.new. For example: + # + # def slow_method(a, b) a ** b end + # + # can be cached using hash_cache :slow_method, which will define the method + # slow_method_cache. We can then find the result of a ** b using: + # + # slow_method_cache[a][b] + # + # The hash structure returned by slow_method_cache would look like this: + # + # Hash.new do |as, a| + # as[a] = Hash.new do |bs, b| + # bs[b] = slow_method(a, b) + # end + # end + # + # The generated code is actually compressed onto a single line to maintain + # sensible backtrace signatures. + # + def hash_cache(method_name, options = {}) + selector = options[:as] || "#{method_name}_cache" + method = self.instance_method(method_name) + + args = [] + code = "def #{selector}(); @#{selector} ||= " + + (1..method.arity).each do |n| + args << "v#{n}" + code << "Hash.new {|h#{n}, v#{n}| h#{n}[v#{n}] = " + end + + # Add the method call with arguments, followed by closing braces and end. + code << "#{method_name}(#{args * ', '}) #{'}' * method.arity} end" + + # Extract the line number information from the caller. Exceptions arising + # in the generated code should point to the +hash_cache :...+ line. + if caller[0] && /^(.*):(\d+)$/ =~ caller[0] + file, line_number = $1, $2.to_i + else # We can't give good trackback info; fallback to this line: + file, line_number = __FILE__, __LINE__ + end + + # We use eval rather than building proc's because it allows us to avoid + # linking the Hash's to this method's binding. Experience has shown that + # doing so can cause obtuse memory leaks. + class_eval code, file, line_number + end + end + + end +end diff --git a/vendor/rails/activesupport/lib/active_support/clean_logger.rb b/vendor/rails/activesupport/lib/active_support/clean_logger.rb new file mode 100644 index 00000000..376896cb --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/clean_logger.rb @@ -0,0 +1,38 @@ +require 'logger' +require File.dirname(__FILE__) + '/core_ext/class/attribute_accessors' + +class Logger #:nodoc: + cattr_accessor :silencer + self.silencer = true + + # Silences the logger for the duration of the block. + def silence(temporary_level = Logger::ERROR) + if silencer + begin + old_logger_level, self.level = level, temporary_level + yield self + ensure + self.level = old_logger_level + end + else + yield self + end + end + + private + alias old_format_message format_message + + # Ruby 1.8.3 transposed the msg and progname arguments to format_message. + # We can't test RUBY_VERSION because some distributions don't keep Ruby + # and its standard library in sync, leading to installations of Ruby 1.8.2 + # with Logger from 1.8.3 and vice versa. + if method_defined?(:formatter=) + def format_message(severity, timestamp, progname, msg) + "#{msg}\n" + end + else + def format_message(severity, timestamp, msg, progname) + "#{msg}\n" + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext.rb b/vendor/rails/activesupport/lib/active_support/core_ext.rb new file mode 100644 index 00000000..573313e7 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext.rb @@ -0,0 +1 @@ +Dir[File.dirname(__FILE__) + "/core_ext/*.rb"].each { |file| require(file) } diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/array.rb b/vendor/rails/activesupport/lib/active_support/core_ext/array.rb new file mode 100644 index 00000000..897d7386 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/array.rb @@ -0,0 +1,21 @@ +require File.dirname(__FILE__) + '/array/conversions' + +class Array #:nodoc: + include ActiveSupport::CoreExtensions::Array::Conversions + + # Iterate over an array in groups of a certain size, padding any remaining + # slots with specified value (nil by default). + # + # E.g. + # + # %w(1 2 3 4 5 6 7).in_groups_of(3) {|g| p g} + # ["1", "2", "3"] + # ["4", "5", "6"] + # ["7", nil, nil] + def in_groups_of(number, fill_with = nil, &block) + require 'enumerator' + collection = dup + collection << fill_with until collection.size.modulo(number).zero? + collection.each_slice(number, &block) + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/array/conversions.rb b/vendor/rails/activesupport/lib/active_support/core_ext/array/conversions.rb new file mode 100644 index 00000000..b516f58b --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -0,0 +1,46 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Array #:nodoc: + module Conversions + # Converts the array to comma-seperated sentence where the last element is joined by the connector word. Options: + # * :connector: The word used to join the last element in arrays with more than two elements (default: "and") + # * :skip_last_comma: Set to true to return "a, b, and c" instead of "a, b and c". + def to_sentence(options = {}) + options.assert_valid_keys(:connector, :skip_last_comma) + options.reverse_merge! :connector => 'and', :skip_last_comma => false + + case length + when 0 + "" + when 1 + self[0] + when 2 + "#{self[0]} #{options[:connector]} #{self[1]}" + else + "#{self[0...-1].join(', ')}#{options[:skip_last_comma] ? '' : ','} #{options[:connector]} #{self[-1]}" + end + end + + # When an array is given to url_for, it is converted to a slash separated string. + def to_param + join '/' + end + + def to_xml(options = {}) + raise "Not all elements respond to to_xml" unless all? { |e| e.respond_to? :to_xml } + + options[:root] ||= all? { |e| e.is_a?(first.class) && first.class.to_s != "Hash" } ? first.class.to_s.underscore.pluralize : "records" + options[:children] ||= options[:root].singularize + options[:indent] ||= 2 + options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent]) + + root = options.delete(:root) + children = options.delete(:children) + + options[:builder].instruct! unless options.delete(:skip_instruct) + options[:builder].tag!(root.to_s.dasherize) { each { |e| e.to_xml(options.merge({ :skip_instruct => true, :root => children })) } } + end + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/blank.rb b/vendor/rails/activesupport/lib/active_support/core_ext/blank.rb new file mode 100644 index 00000000..f7fbea3e --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/blank.rb @@ -0,0 +1,50 @@ +class Object #:nodoc: + # "", " ", nil, [], and {} are blank + def blank? + if respond_to?(:empty?) && respond_to?(:strip) + empty? or strip.empty? + elsif respond_to?(:empty?) + empty? + else + !self + end + end +end + +class NilClass #:nodoc: + def blank? + true + end +end + +class FalseClass #:nodoc: + def blank? + true + end +end + +class TrueClass #:nodoc: + def blank? + false + end +end + +class Array #:nodoc: + alias_method :blank?, :empty? +end + +class Hash #:nodoc: + alias_method :blank?, :empty? +end + +class String #:nodoc: + def blank? + empty? || strip.empty? + end +end + +class Numeric #:nodoc: + def blank? + false + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/cgi.rb b/vendor/rails/activesupport/lib/active_support/core_ext/cgi.rb new file mode 100644 index 00000000..072a7c99 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/cgi.rb @@ -0,0 +1,5 @@ +require File.dirname(__FILE__) + '/cgi/escape_skipping_slashes' + +class CGI #:nodoc: + extend(ActiveSupport::CoreExtensions::CGI::EscapeSkippingSlashes) +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/cgi/escape_skipping_slashes.rb b/vendor/rails/activesupport/lib/active_support/core_ext/cgi/escape_skipping_slashes.rb new file mode 100644 index 00000000..a21e98fa --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/cgi/escape_skipping_slashes.rb @@ -0,0 +1,14 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module CGI #:nodoc: + module EscapeSkippingSlashes #:nodoc: + def escape_skipping_slashes(str) + str = str.join('/') if str.respond_to? :join + str.gsub(/([^ \/a-zA-Z0-9_.-])/n) do + "%#{$1.unpack('H2').first.upcase}" + end.tr(' ', '+') + end + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/class.rb b/vendor/rails/activesupport/lib/active_support/core_ext/class.rb new file mode 100644 index 00000000..7bacdb39 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/class.rb @@ -0,0 +1,3 @@ +require File.dirname(__FILE__) + '/class/attribute_accessors' +require File.dirname(__FILE__) + '/class/inheritable_attributes' +require File.dirname(__FILE__) + '/class/removal' \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb b/vendor/rails/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb new file mode 100644 index 00000000..93a7d48f --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb @@ -0,0 +1,44 @@ +# Extends the class object with class and instance accessors for class attributes, +# just like the native attr* accessors for instance attributes. +class Class # :nodoc: + def cattr_reader(*syms) + syms.flatten.each do |sym| + class_eval(<<-EOS, __FILE__, __LINE__) + unless defined? @@#{sym} + @@#{sym} = nil + end + + def self.#{sym} + @@#{sym} + end + + def #{sym} + @@#{sym} + end + EOS + end + end + + def cattr_writer(*syms) + syms.flatten.each do |sym| + class_eval(<<-EOS, __FILE__, __LINE__) + unless defined? @@#{sym} + @@#{sym} = nil + end + + def self.#{sym}=(obj) + @@#{sym} = obj + end + + def #{sym}=(obj) + @@#{sym} = obj + end + EOS + end + end + + def cattr_accessor(*syms) + cattr_reader(*syms) + cattr_writer(*syms) + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb b/vendor/rails/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb new file mode 100644 index 00000000..2e70a644 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb @@ -0,0 +1,115 @@ +# Retain for backward compatibility. Methods are now included in Class. +module ClassInheritableAttributes # :nodoc: +end + +# Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of +# their parents' attributes, instead of just a pointer to the same. This means that the child can add elements +# to, for example, an array without those additions being shared with either their parent, siblings, or +# children, which is unlike the regular class-level attributes that are shared across the entire hierarchy. +class Class # :nodoc: + def class_inheritable_reader(*syms) + syms.each do |sym| + class_eval <<-EOS + def self.#{sym} + read_inheritable_attribute(:#{sym}) + end + + def #{sym} + self.class.#{sym} + end + EOS + end + end + + def class_inheritable_writer(*syms) + syms.each do |sym| + class_eval <<-EOS + def self.#{sym}=(obj) + write_inheritable_attribute(:#{sym}, obj) + end + + def #{sym}=(obj) + self.class.#{sym} = obj + end + EOS + end + end + + def class_inheritable_array_writer(*syms) + syms.each do |sym| + class_eval <<-EOS + def self.#{sym}=(obj) + write_inheritable_array(:#{sym}, obj) + end + + def #{sym}=(obj) + self.class.#{sym} = obj + end + EOS + end + end + + def class_inheritable_hash_writer(*syms) + syms.each do |sym| + class_eval <<-EOS + def self.#{sym}=(obj) + write_inheritable_hash(:#{sym}, obj) + end + + def #{sym}=(obj) + self.class.#{sym} = obj + end + EOS + end + end + + def class_inheritable_accessor(*syms) + class_inheritable_reader(*syms) + class_inheritable_writer(*syms) + end + + def class_inheritable_array(*syms) + class_inheritable_reader(*syms) + class_inheritable_array_writer(*syms) + end + + def class_inheritable_hash(*syms) + class_inheritable_reader(*syms) + class_inheritable_hash_writer(*syms) + end + + def inheritable_attributes + @inheritable_attributes ||= {} + end + + def write_inheritable_attribute(key, value) + inheritable_attributes[key] = value + end + + def write_inheritable_array(key, elements) + write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil? + write_inheritable_attribute(key, read_inheritable_attribute(key) + elements) + end + + def write_inheritable_hash(key, hash) + write_inheritable_attribute(key, {}) if read_inheritable_attribute(key).nil? + write_inheritable_attribute(key, read_inheritable_attribute(key).merge(hash)) + end + + def read_inheritable_attribute(key) + inheritable_attributes[key] + end + + def reset_inheritable_attributes + inheritable_attributes.clear + end + + private + def inherited_with_inheritable_attributes(child) + inherited_without_inheritable_attributes(child) if respond_to?(:inherited_without_inheritable_attributes) + child.instance_variable_set('@inheritable_attributes', inheritable_attributes.dup) + end + + alias inherited_without_inheritable_attributes inherited + alias inherited inherited_with_inheritable_attributes +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/class/removal.rb b/vendor/rails/activesupport/lib/active_support/core_ext/class/removal.rb new file mode 100644 index 00000000..b217c195 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/class/removal.rb @@ -0,0 +1,24 @@ +class Class #:nodoc: + def remove_subclasses + Object.remove_subclasses_of(self) + end + + def subclasses + Object.subclasses_of(self).map { |o| o.to_s } + end + + def remove_class(*klasses) + klasses.flatten.each do |klass| + # Skip this class if there is nothing bound to this name + next unless defined?(klass.name) + + basename = klass.to_s.split("::").last + parent = klass.parent + + # Skip this class if it does not match the current one bound to this name + next unless parent.const_defined?(basename) && klass = parent.const_get(basename) + + parent.send :remove_const, basename unless parent == klass + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/date.rb b/vendor/rails/activesupport/lib/active_support/core_ext/date.rb new file mode 100644 index 00000000..239b8c14 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/date.rb @@ -0,0 +1,6 @@ +require 'date' +require File.dirname(__FILE__) + '/date/conversions' + +class Date#:nodoc: + include ActiveSupport::CoreExtensions::Date::Conversions +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/date/conversions.rb b/vendor/rails/activesupport/lib/active_support/core_ext/date/conversions.rb new file mode 100644 index 00000000..627c8dd4 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -0,0 +1,33 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Date #:nodoc: + # Getting dates in different convenient string representations and other objects + module Conversions + DATE_FORMATS = { + :short => "%e %b", + :long => "%B %e, %Y" + } + + def self.included(klass) #:nodoc: + klass.send(:alias_method, :to_default_s, :to_s) + klass.send(:alias_method, :to_s, :to_formatted_s) + end + + def to_formatted_s(format = :default) + DATE_FORMATS[format] ? strftime(DATE_FORMATS[format]).strip : to_default_s + end + + # To be able to keep Dates and Times interchangeable on conversions + def to_date + self + end + + def to_time(form = :local) + ::Time.send(form, year, month, day) + end + + alias :xmlschema :to_s + end + end + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/enumerable.rb b/vendor/rails/activesupport/lib/active_support/core_ext/enumerable.rb new file mode 100644 index 00000000..a49c4c70 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/enumerable.rb @@ -0,0 +1,31 @@ +module Enumerable #:nodoc: + def first_match + match = nil + each do |items| + break if match = yield(items) + end + match + end + + # Collect an enumerable into sets, grouped by the result of a block. Useful, + # for example, for grouping records by date. + # + # e.g. + # + # latest_transcripts.group_by(&:day).each do |day, transcripts| + # p "#{day} -> #{transcripts.map(&:class) * ', '}" + # end + # "2006-03-01 -> Transcript" + # "2006-02-28 -> Transcript" + # "2006-02-27 -> Transcript, Transcript" + # "2006-02-26 -> Transcript, Transcript" + # "2006-02-25 -> Transcript" + # "2006-02-24 -> Transcript, Transcript" + # "2006-02-23 -> Transcript" + def group_by + inject({}) do |groups, element| + (groups[yield(element)] ||= []) << element + groups + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/exception.rb b/vendor/rails/activesupport/lib/active_support/core_ext/exception.rb new file mode 100644 index 00000000..2e396511 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/exception.rb @@ -0,0 +1,33 @@ +class Exception # :nodoc: + def clean_message + Pathname.clean_within message + end + + TraceSubstitutions = [] + FrameworkRegexp = /generated|vendor|dispatch|ruby|script\/\w+/ + + def clean_backtrace + backtrace.collect do |line| + Pathname.clean_within(TraceSubstitutions.inject(line) do |line, (regexp, sub)| + line.gsub regexp, sub + end) + end + end + + def application_backtrace + before_application_frame = true + + trace = clean_backtrace.reject do |line| + non_app_frame = (line =~ FrameworkRegexp) + before_application_frame = false unless non_app_frame + non_app_frame && ! before_application_frame + end + + # If we didn't find any application frames, return an empty app trace. + before_application_frame ? [] : trace + end + + def framework_backtrace + clean_backtrace.select {|line| line =~ FrameworkRegexp} + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/hash.rb b/vendor/rails/activesupport/lib/active_support/core_ext/hash.rb new file mode 100644 index 00000000..7d94d709 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/hash.rb @@ -0,0 +1,13 @@ +require File.dirname(__FILE__) + '/hash/keys' +require File.dirname(__FILE__) + '/hash/indifferent_access' +require File.dirname(__FILE__) + '/hash/reverse_merge' +require File.dirname(__FILE__) + '/hash/conversions' +require File.dirname(__FILE__) + '/hash/diff' + +class Hash #:nodoc: + include ActiveSupport::CoreExtensions::Hash::Keys + include ActiveSupport::CoreExtensions::Hash::IndifferentAccess + include ActiveSupport::CoreExtensions::Hash::ReverseMerge + include ActiveSupport::CoreExtensions::Hash::Conversions + include ActiveSupport::CoreExtensions::Hash::Diff +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/hash/conversions.rb b/vendor/rails/activesupport/lib/active_support/core_ext/hash/conversions.rb new file mode 100644 index 00000000..d556bab0 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -0,0 +1,44 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Hash #:nodoc: + module Conversions + XML_TYPE_NAMES = { + "Fixnum" => "integer", + "Date" => "date", + "Time" => "datetime", + "TrueClass" => "boolean", + "FalseClass" => "boolean" + } + + XML_FORMATTING = { + "date" => Proc.new { |date| date.to_s(:db) }, + "datetime" => Proc.new { |time| time.xmlschema } + } + + def to_xml(options = {}) + options[:indent] ||= 2 + options.reverse_merge!({ :builder => Builder::XmlMarkup.new(:indent => options[:indent]), :root => "hash" }) + options[:builder].instruct! unless options.delete(:skip_instruct) + + options[:builder].__send__(options[:root].to_s.dasherize) do + each do |key, value| + case value + when ::Hash + value.to_xml(options.merge({ :root => key, :skip_instruct => true })) + when ::Array + value.to_xml(options.merge({ :root => key, :children => key.to_s.singularize, :skip_instruct => true})) + else + type_name = XML_TYPE_NAMES[value.class.to_s] + + options[:builder].tag!(key.to_s.dasherize, + XML_FORMATTING[type_name] ? XML_FORMATTING[type_name].call(value) : value, + options[:skip_types] || value.nil? || type_name.nil? ? { } : { :type => type_name } + ) + end + end + end + end + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/hash/diff.rb b/vendor/rails/activesupport/lib/active_support/core_ext/hash/diff.rb new file mode 100644 index 00000000..deace40a --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/hash/diff.rb @@ -0,0 +1,11 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Hash #:nodoc: + module Diff + def diff(h2) + self.dup.delete_if { |k, v| h2[k] == v }.merge(h2.dup.delete_if { |k, v| self.has_key?(k) }) + end + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/vendor/rails/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb new file mode 100644 index 00000000..472e3ab7 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb @@ -0,0 +1,79 @@ +# this class has dubious semantics and we only have it so that +# people can write params[:key] instead of params['key'] + +class HashWithIndifferentAccess < Hash + def initialize(constructor = {}) + if constructor.is_a?(Hash) + super() + update(constructor) + else + super(constructor) + end + end + + def default(key) + self[key.to_s] if key.is_a?(Symbol) + end + + alias_method :regular_writer, :[]= unless method_defined?(:regular_writer) + alias_method :regular_update, :update unless method_defined?(:regular_update) + + def []=(key, value) + regular_writer(convert_key(key), convert_value(value)) + end + + def update(other_hash) + other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) } + self + end + + alias_method :merge!, :update + + def key?(key) + super(convert_key(key)) + end + + alias_method :include?, :key? + alias_method :has_key?, :key? + alias_method :member?, :key? + + def fetch(key, *extras) + super(convert_key(key), *extras) + end + + def values_at(*indices) + indices.collect {|key| self[convert_key(key)]} + end + + def dup + HashWithIndifferentAccess.new(self) + end + + def merge(hash) + self.dup.update(hash) + end + + def delete(key) + super(convert_key(key)) + end + + protected + def convert_key(key) + key.kind_of?(Symbol) ? key.to_s : key + end + def convert_value(value) + value.is_a?(Hash) ? value.with_indifferent_access : value + end +end + +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Hash #:nodoc: + module IndifferentAccess #:nodoc: + def with_indifferent_access + HashWithIndifferentAccess.new(self) + end + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/hash/keys.rb b/vendor/rails/activesupport/lib/active_support/core_ext/hash/keys.rb new file mode 100644 index 00000000..3c8a59f0 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -0,0 +1,53 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Hash #:nodoc: + module Keys + # Return a new hash with all keys converted to strings. + def stringify_keys + inject({}) do |options, (key, value)| + options[key.to_s] = value + options + end + end + + # Destructively convert all keys to strings. + def stringify_keys! + keys.each do |key| + unless key.class.to_s == "String" # weird hack to make the tests run when string_ext_test.rb is also running + self[key.to_s] = self[key] + delete(key) + end + end + self + end + + # Return a new hash with all keys converted to symbols. + def symbolize_keys + inject({}) do |options, (key, value)| + options[key.to_sym] = value + options + end + end + + # Destructively convert all keys to symbols. + def symbolize_keys! + keys.each do |key| + unless key.is_a?(Symbol) + self[key.to_sym] = self[key] + delete(key) + end + end + self + end + + alias_method :to_options, :symbolize_keys + alias_method :to_options!, :symbolize_keys! + + def assert_valid_keys(*valid_keys) + unknown_keys = keys - [valid_keys].flatten + raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty? + end + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb b/vendor/rails/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb new file mode 100644 index 00000000..46c53871 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb @@ -0,0 +1,25 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Hash #:nodoc: + # Allows for reverse merging where its the keys in the calling hash that wins over those in the other_hash. + # This is particularly useful for initializing an incoming option hash with default values: + # + # def setup(options = {}) + # options.reverse_merge! :size => 25, :velocity => 10 + # end + # + # The default :size and :velocity is only set if the +options+ passed in doesn't already have those keys set. + module ReverseMerge + def reverse_merge(other_hash) + other_hash.merge(self) + end + + def reverse_merge!(other_hash) + replace(reverse_merge(other_hash)) + end + + alias_method :reverse_update, :reverse_merge + end + end + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/integer.rb b/vendor/rails/activesupport/lib/active_support/core_ext/integer.rb new file mode 100644 index 00000000..9346b88f --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/integer.rb @@ -0,0 +1,7 @@ +require File.dirname(__FILE__) + '/integer/even_odd' +require File.dirname(__FILE__) + '/integer/inflections' + +class Integer #:nodoc: + include ActiveSupport::CoreExtensions::Integer::EvenOdd + include ActiveSupport::CoreExtensions::Integer::Inflections +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/integer/even_odd.rb b/vendor/rails/activesupport/lib/active_support/core_ext/integer/even_odd.rb new file mode 100644 index 00000000..3762308c --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/integer/even_odd.rb @@ -0,0 +1,24 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Integer #:nodoc: + # For checking if a fixnum is even or odd. + # * 1.even? # => false + # * 1.odd? # => true + # * 2.even? # => true + # * 2.odd? # => false + module EvenOdd + def multiple_of?(number) + self % number == 0 + end + + def even? + multiple_of? 2 + end + + def odd? + !even? + end + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/integer/inflections.rb b/vendor/rails/activesupport/lib/active_support/core_ext/integer/inflections.rb new file mode 100644 index 00000000..94721cfc --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/integer/inflections.rb @@ -0,0 +1,15 @@ +require File.dirname(__FILE__) + '/../../inflector' unless defined? Inflector +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Integer #:nodoc: + module Inflections + # 1.ordinalize # => "1st" + # 3.ordinalize # => "3rd" + # 10.ordinalize # => "10th" + def ordinalize + Inflector.ordinalize(self) + end + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/kernel.rb b/vendor/rails/activesupport/lib/active_support/core_ext/kernel.rb new file mode 100644 index 00000000..1aa4f72b --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/kernel.rb @@ -0,0 +1,4 @@ +require File.dirname(__FILE__) + '/kernel/daemonizing' +require File.dirname(__FILE__) + '/kernel/reporting' +require File.dirname(__FILE__) + '/kernel/agnostics' +require File.dirname(__FILE__) + '/kernel/requires' diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/kernel/agnostics.rb b/vendor/rails/activesupport/lib/active_support/core_ext/kernel/agnostics.rb new file mode 100644 index 00000000..c0cb4fb4 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/kernel/agnostics.rb @@ -0,0 +1,11 @@ +class Object + # Makes backticks behave (somewhat more) similarly on all platforms. + # On win32 `nonexistent_command` raises Errno::ENOENT; on Unix, the + # spawned shell prints a message to stderr and sets $?. We emulate + # Unix on the former but not the latter. + def `(command) #:nodoc: + super + rescue Errno::ENOENT => e + STDERR.puts "#$0: #{e}" + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/kernel/daemonizing.rb b/vendor/rails/activesupport/lib/active_support/core_ext/kernel/daemonizing.rb new file mode 100644 index 00000000..0e78819f --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/kernel/daemonizing.rb @@ -0,0 +1,15 @@ +module Kernel + # Turns the current script into a daemon process that detaches from the console. + # It can be shut down with a TERM signal. + def daemonize + exit if fork # Parent exits, child continues. + Process.setsid # Become session leader. + exit if fork # Zap session leader. See [1]. + Dir.chdir "/" # Release old working directory. + File.umask 0000 # Ensure sensible umask. Adjust as needed. + STDIN.reopen "/dev/null" # Free file descriptors and + STDOUT.reopen "/dev/null", "a" # point them somewhere sensible. + STDERR.reopen STDOUT # STDOUT/ERR should better go to a logfile. + trap("TERM") { exit } + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/vendor/rails/activesupport/lib/active_support/core_ext/kernel/reporting.rb new file mode 100644 index 00000000..a5cec502 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -0,0 +1,51 @@ +module Kernel + # Sets $VERBOSE to nil for the duration of the block and back to its original value afterwards. + # + # silence_warnings do + # value = noisy_call # no warning voiced + # end + # + # noisy_call # warning voiced + def silence_warnings + old_verbose, $VERBOSE = $VERBOSE, nil + yield + ensure + $VERBOSE = old_verbose + end + + # Sets $VERBOSE to true for the duration of the block and back to its original value afterwards. + def enable_warnings + old_verbose, $VERBOSE = $VERBOSE, true + yield + ensure + $VERBOSE = old_verbose + end + + # For compatibility + def silence_stderr #:nodoc: + silence_stream(STDERR) { yield } + end + + # Silences any stream for the duration of the block. + # + # silence_stream(STDOUT) do + # puts 'This will never be seen' + # end + # + # puts 'But this will' + def silence_stream(stream) + old_stream = stream.dup + stream.reopen(RUBY_PLATFORM =~ /mswin/ ? 'NUL:' : '/dev/null') + stream.sync = true + yield + ensure + stream.reopen(old_stream) + end + + def suppress(*exception_classes) + begin yield + rescue Exception => e + raise unless exception_classes.any? { |cls| e.kind_of?(cls) } + end + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/kernel/requires.rb b/vendor/rails/activesupport/lib/active_support/core_ext/kernel/requires.rb new file mode 100644 index 00000000..323fea49 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/kernel/requires.rb @@ -0,0 +1,24 @@ +module Kernel + # Require a library with fallback to RubyGems. Warnings during library + # loading are silenced to increase signal/noise for application warnings. + def require_library_or_gem(library_name) + silence_warnings do + begin + require library_name + rescue LoadError => cannot_require + # 1. Requiring the module is unsuccessful, maybe it's a gem and nobody required rubygems yet. Try. + begin + require 'rubygems' + rescue LoadError => rubygems_not_installed + raise cannot_require + end + # 2. Rubygems is installed and loaded. Try to load the library again + begin + require library_name + rescue LoadError => gem_not_installed + raise cannot_require + end + end + end + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/load_error.rb b/vendor/rails/activesupport/lib/active_support/core_ext/load_error.rb new file mode 100644 index 00000000..fac4639e --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/load_error.rb @@ -0,0 +1,38 @@ +class MissingSourceFile < LoadError #:nodoc: + attr_reader :path + def initialize(message, path) + super(message) + @path = path + end + + def is_missing?(path) + path.gsub(/\.rb$/, '') == self.path.gsub(/\.rb$/, '') + end + + def self.from_message(message) + REGEXPS.each do |regexp, capture| + match = regexp.match(message) + return MissingSourceFile.new(message, match[capture]) unless match.nil? + end + nil + end + + REGEXPS = [ + [/^no such file to load -- (.+)$/i, 1], + [/^Missing \w+ (file\s*)?([^\s]+.rb)$/i, 2], + [/^Missing API definition file in (.+)$/i, 1] + ] +end + +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module LoadErrorExtensions #:nodoc: + module LoadErrorClassMethods #:nodoc: + def new(*args) + (self == LoadError && MissingSourceFile.from_message(args.first)) || super + end + end + ::LoadError.extend(LoadErrorClassMethods) + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/logger.rb b/vendor/rails/activesupport/lib/active_support/core_ext/logger.rb new file mode 100644 index 00000000..9c1fd274 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/logger.rb @@ -0,0 +1,16 @@ +# Adds the 'around_level' method to Logger. + +class Logger + def self.define_around_helper(level) + module_eval <<-end_eval + def around_#{level}(before_message, after_message, &block) + self.#{level}(before_message) + return_value = block.call(self) + self.#{level}(after_message) + return return_value + end + end_eval + end + [:debug, :info, :error, :fatal].each {|level| define_around_helper(level) } + +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/module.rb b/vendor/rails/activesupport/lib/active_support/core_ext/module.rb new file mode 100644 index 00000000..e67cf940 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/module.rb @@ -0,0 +1,5 @@ +require File.dirname(__FILE__) + '/module/inclusion' +require File.dirname(__FILE__) + '/module/attribute_accessors' +require File.dirname(__FILE__) + '/module/delegation' +require File.dirname(__FILE__) + '/module/introspection' +require File.dirname(__FILE__) + '/module/loading' diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/vendor/rails/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb new file mode 100644 index 00000000..fe4f8a4f --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -0,0 +1,44 @@ +# Extends the module object with module and instance accessors for class attributes, +# just like the native attr* accessors for instance attributes. +class Module # :nodoc: + def mattr_reader(*syms) + syms.each do |sym| + class_eval(<<-EOS, __FILE__, __LINE__) + unless defined? @@#{sym} + @@#{sym} = nil + end + + def self.#{sym} + @@#{sym} + end + + def #{sym} + @@#{sym} + end + EOS + end + end + + def mattr_writer(*syms) + syms.each do |sym| + class_eval(<<-EOS, __FILE__, __LINE__) + unless defined? @@#{sym} + @@#{sym} = nil + end + + def self.#{sym}=(obj) + @@#{sym} = obj + end + + def #{sym}=(obj) + @@#{sym} = obj + end + EOS + end + end + + def mattr_accessor(*syms) + mattr_reader(*syms) + mattr_writer(*syms) + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/module/delegation.rb b/vendor/rails/activesupport/lib/active_support/core_ext/module/delegation.rb new file mode 100644 index 00000000..95173007 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -0,0 +1,16 @@ +class Module + def delegate(*methods) + options = methods.pop + unless options.is_a?(Hash) && to = options[:to] + raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key" + end + + methods.each do |method| + module_eval(<<-EOS, "(__DELEGATION__)", 1) + def #{method}(*args, &block) + #{to}.__send__(#{method.inspect}, *args, &block) + end + EOS + end + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/module/inclusion.rb b/vendor/rails/activesupport/lib/active_support/core_ext/module/inclusion.rb new file mode 100644 index 00000000..efc00d6f --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/module/inclusion.rb @@ -0,0 +1,11 @@ +class Module + def included_in_classes + classes = [] + ObjectSpace.each_object(Class) { |k| classes << k if k.included_modules.include?(self) } + + classes.reverse.inject([]) do |unique_classes, klass| + unique_classes << klass unless unique_classes.collect { |k| k.to_s }.include?(klass.to_s) + unique_classes + end + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/module/introspection.rb b/vendor/rails/activesupport/lib/active_support/core_ext/module/introspection.rb new file mode 100644 index 00000000..0cd0d1ff --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/module/introspection.rb @@ -0,0 +1,21 @@ +class Module + # Return the module which contains this one; if this is a root module, such as + # +::MyModule+, then Object is returned. + def parent + parent_name = name.split('::')[0..-2] * '::' + parent_name.empty? ? Object : parent_name.constantize + end + + # Return all the parents of this module, ordered from nested outwards. The + # receiver is not contained within the result. + def parents + parents = [] + parts = name.split('::')[0..-2] + until parts.empty? + parents << (parts * '::').constantize + parts.pop + end + parents << Object unless parents.include? Object + parents + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/module/loading.rb b/vendor/rails/activesupport/lib/active_support/core_ext/module/loading.rb new file mode 100644 index 00000000..36c0c614 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/module/loading.rb @@ -0,0 +1,13 @@ +class Module + def as_load_path + if self == Object || self == Kernel + '' + elsif is_a? Class + parent == self ? '' : parent.as_load_path + else + name.split('::').collect do |word| + word.underscore + end * '/' + end + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/numeric.rb b/vendor/rails/activesupport/lib/active_support/core_ext/numeric.rb new file mode 100644 index 00000000..88fead7a --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/numeric.rb @@ -0,0 +1,7 @@ +require File.dirname(__FILE__) + '/numeric/time' +require File.dirname(__FILE__) + '/numeric/bytes' + +class Numeric #:nodoc: + include ActiveSupport::CoreExtensions::Numeric::Time + include ActiveSupport::CoreExtensions::Numeric::Bytes +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/numeric/bytes.rb b/vendor/rails/activesupport/lib/active_support/core_ext/numeric/bytes.rb new file mode 100644 index 00000000..56477673 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/numeric/bytes.rb @@ -0,0 +1,44 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Numeric #:nodoc: + # Enables the use of byte calculations and declarations, like 45.bytes + 2.6.megabytes + module Bytes + def bytes + self + end + alias :byte :bytes + + def kilobytes + self * 1024 + end + alias :kilobyte :kilobytes + + def megabytes + self * 1024.kilobytes + end + alias :megabyte :megabytes + + def gigabytes + self * 1024.megabytes + end + alias :gigabyte :gigabytes + + def terabytes + self * 1024.gigabytes + end + alias :terabyte :terabytes + + def petabytes + self * 1024.terabytes + end + alias :petabyte :petabytes + + def exabytes + self * 1024.petabytes + end + alias :exabyte :exabytes + + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/numeric/time.rb b/vendor/rails/activesupport/lib/active_support/core_ext/numeric/time.rb new file mode 100644 index 00000000..93740046 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -0,0 +1,72 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Numeric #:nodoc: + # Enables the use of time calculations and declarations, like 45.minutes + 2.hours + 4.years. + # + # If you need precise date calculations that doesn't just treat months as 30 days, then have + # a look at Time#advance. + # + # Some of these methods are approximations, Ruby's core + # Date[http://stdlib.rubyonrails.org/libdoc/date/rdoc/index.html] and + # Time[http://stdlib.rubyonrails.org/libdoc/time/rdoc/index.html] should be used for precision + # date and time arithmetic + module Time + def seconds + self + end + alias :second :seconds + + def minutes + self * 60 + end + alias :minute :minutes + + def hours + self * 60.minutes + end + alias :hour :hours + + def days + self * 24.hours + end + alias :day :days + + def weeks + self * 7.days + end + alias :week :weeks + + def fortnights + self * 2.weeks + end + alias :fortnight :fortnights + + def months + self * 30.days + end + alias :month :months + + def years + (self * 365.25.days).to_i + end + alias :year :years + + # Reads best without arguments: 10.minutes.ago + def ago(time = ::Time.now) + time - self + end + + # Reads best with argument: 10.minutes.until(time) + alias :until :ago + + # Reads best with argument: 10.minutes.since(time) + def since(time = ::Time.now) + time + self + end + + # Reads best without arguments: 10.minutes.from_now + alias :from_now :since + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/object.rb b/vendor/rails/activesupport/lib/active_support/core_ext/object.rb new file mode 100644 index 00000000..3dd6deab --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/object.rb @@ -0,0 +1,2 @@ +require File.dirname(__FILE__) + '/object/extending' +require File.dirname(__FILE__) + '/object/misc' \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/object/extending.rb b/vendor/rails/activesupport/lib/active_support/core_ext/object/extending.rb new file mode 100644 index 00000000..e15b4bf3 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/object/extending.rb @@ -0,0 +1,47 @@ +class Object #:nodoc: + def remove_subclasses_of(*superclasses) + Class.remove_class(*subclasses_of(*superclasses)) + end + + def subclasses_of(*superclasses) + subclasses = [] + ObjectSpace.each_object(Class) do |k| + next if # Exclude this class if + (k.ancestors & superclasses).empty? || # It's not a subclass of our supers + superclasses.include?(k) || # It *is* one of the supers + eval("! defined?(::#{k})") || # It's not defined. + eval("::#{k}").object_id != k.object_id + subclasses << k + end + subclasses + end + + def extended_by + ancestors = class << self; ancestors end + ancestors.select { |mod| mod.class == Module } - [ Object, Kernel ] + end + + def copy_instance_variables_from(object, exclude = []) + exclude += object.protected_instance_variables if object.respond_to? :protected_instance_variables + + instance_variables = object.instance_variables - exclude.map { |name| name.to_s } + instance_variables.each { |name| instance_variable_set(name, object.instance_variable_get(name)) } + end + + def extend_with_included_modules_from(object) + object.extended_by.each { |mod| extend mod } + end + + def instance_values + instance_variables.inject({}) do |values, name| + values[name[1..-1]] = instance_variable_get(name) + values + end + end + + unless defined? instance_exec # 1.9 + def instance_exec(*arguments, &block) + block.bind(self)[*arguments] + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/object/misc.rb b/vendor/rails/activesupport/lib/active_support/core_ext/object/misc.rb new file mode 100644 index 00000000..ea8e3a1d --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/object/misc.rb @@ -0,0 +1,34 @@ +class Object #:nodoc: + # A Ruby-ized realization of the K combinator, courtesy of Mikael Brockman. + # + # def foo + # returning values = [] do + # values << 'bar' + # values << 'baz' + # end + # end + # + # foo # => ['bar', 'baz'] + # + # def foo + # returning [] do |values| + # values << 'bar' + # values << 'baz' + # end + # end + # + # foo # => ['bar', 'baz'] + # + def returning(value) + yield(value) + value + end + + def with_options(options) + yield ActiveSupport::OptionMerger.new(self, options) + end + + def to_json + ActiveSupport::JSON.encode(self) + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/pathname.rb b/vendor/rails/activesupport/lib/active_support/core_ext/pathname.rb new file mode 100644 index 00000000..9e78c273 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/pathname.rb @@ -0,0 +1,7 @@ +require 'pathname' +require File.dirname(__FILE__) + '/pathname/clean_within' + +class Pathname#:nodoc: + extend ActiveSupport::CoreExtensions::Pathname::CleanWithin +end + diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/pathname/clean_within.rb b/vendor/rails/activesupport/lib/active_support/core_ext/pathname/clean_within.rb new file mode 100644 index 00000000..ae03e1bc --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/pathname/clean_within.rb @@ -0,0 +1,14 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Pathname #:nodoc: + module CleanWithin + # Clean the paths contained in the provided string. + def clean_within(string) + string.gsub(%r{[\w. ]+(/[\w. ]+)+(\.rb)?(\b|$)}) do |path| + new(path).cleanpath + end + end + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/proc.rb b/vendor/rails/activesupport/lib/active_support/core_ext/proc.rb new file mode 100644 index 00000000..2ca23f62 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/proc.rb @@ -0,0 +1,12 @@ +class Proc #:nodoc: + def bind(object) + block, time = self, Time.now + (class << object; self end).class_eval do + method_name = "__bind_#{time.to_i}_#{time.usec}" + define_method(method_name, &block) + method = instance_method(method_name) + remove_method(method_name) + method + end.bind(object) + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/range.rb b/vendor/rails/activesupport/lib/active_support/core_ext/range.rb new file mode 100644 index 00000000..ca775115 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/range.rb @@ -0,0 +1,5 @@ +require File.dirname(__FILE__) + '/range/conversions' + +class Range #:nodoc: + include ActiveSupport::CoreExtensions::Range::Conversions +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/range/conversions.rb b/vendor/rails/activesupport/lib/active_support/core_ext/range/conversions.rb new file mode 100644 index 00000000..677ba639 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/range/conversions.rb @@ -0,0 +1,21 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Range #:nodoc: + # Getting dates in different convenient string representations and other objects + module Conversions + DATE_FORMATS = { + :db => Proc.new { |start, stop| "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'" } + } + + def self.included(klass) #:nodoc: + klass.send(:alias_method, :to_default_s, :to_s) + klass.send(:alias_method, :to_s, :to_formatted_s) + end + + def to_formatted_s(format = :default) + DATE_FORMATS[format] ? DATE_FORMATS[format].call(first, last) : to_default_s + end + end + end + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/string.rb b/vendor/rails/activesupport/lib/active_support/core_ext/string.rb new file mode 100644 index 00000000..240e1ff1 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/string.rb @@ -0,0 +1,13 @@ +require File.dirname(__FILE__) + '/string/inflections' +require File.dirname(__FILE__) + '/string/conversions' +require File.dirname(__FILE__) + '/string/access' +require File.dirname(__FILE__) + '/string/starts_ends_with' +require File.dirname(__FILE__) + '/string/iterators' + +class String #:nodoc: + include ActiveSupport::CoreExtensions::String::Access + include ActiveSupport::CoreExtensions::String::Conversions + include ActiveSupport::CoreExtensions::String::Inflections + include ActiveSupport::CoreExtensions::String::StartsEndsWith + include ActiveSupport::CoreExtensions::String::Iterators +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/string/access.rb b/vendor/rails/activesupport/lib/active_support/core_ext/string/access.rb new file mode 100644 index 00000000..5d0e0c21 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/string/access.rb @@ -0,0 +1,58 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module String #:nodoc: + # Makes it easier to access parts of a string, such as specific characters and substrings. + module Access + # Returns the character at the +position+ treating the string as an array (where 0 is the first character). + # + # Examples: + # "hello".at(0) # => "h" + # "hello".at(4) # => "o" + # "hello".at(10) # => nil + def at(position) + self[position, 1] + end + + # Returns the remaining of the string from the +position+ treating the string as an array (where 0 is the first character). + # + # Examples: + # "hello".from(0) # => "hello" + # "hello".from(2) # => "llo" + # "hello".from(10) # => nil + def from(position) + self[position..-1] + end + + # Returns the beginning of the string up to the +position+ treating the string as an array (where 0 is the first character). + # + # Examples: + # "hello".to(0) # => "h" + # "hello".to(2) # => "hel" + # "hello".to(10) # => "hello" + def to(position) + self[0..position] + end + + # Returns the first character of the string or the first +limit+ characters. + # + # Examples: + # "hello".first # => "h" + # "hello".first(2) # => "he" + # "hello".first(10) # => "hello" + def first(limit = 1) + self[0..(limit - 1)] + end + + # Returns the last character of the string or the last +limit+ characters. + # + # Examples: + # "hello".last # => "o" + # "hello".last(2) # => "lo" + # "hello".last(10) # => "hello" + def last(limit = 1) + self[(-limit)..-1] || self + end + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/string/conversions.rb b/vendor/rails/activesupport/lib/active_support/core_ext/string/conversions.rb new file mode 100644 index 00000000..cfb767d1 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/string/conversions.rb @@ -0,0 +1,19 @@ +require 'parsedate' + +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module String #:nodoc: + # Converting strings to other objects + module Conversions + # Form can be either :utc (default) or :local. + def to_time(form = :utc) + ::Time.send(form, *ParseDate.parsedate(self)) + end + + def to_date + ::Date.new(*ParseDate.parsedate(self)[0..2]) + end + end + end + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/string/inflections.rb b/vendor/rails/activesupport/lib/active_support/core_ext/string/inflections.rb new file mode 100644 index 00000000..07291032 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -0,0 +1,64 @@ +require File.dirname(__FILE__) + '/../../inflector' unless defined? Inflector +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module String #:nodoc: + # Makes it possible to do "posts".singularize that returns "post" and "MegaCoolClass".underscore that returns "mega_cool_class". + module Inflections + def pluralize + Inflector.pluralize(self) + end + + def singularize + Inflector.singularize(self) + end + + def camelize(first_letter = :upper) + case first_letter + when :upper then Inflector.camelize(self, true) + when :lower then Inflector.camelize(self, false) + end + end + alias_method :camelcase, :camelize + + def titleize + Inflector.titleize(self) + end + alias_method :titlecase, :titleize + + def underscore + Inflector.underscore(self) + end + + def dasherize + Inflector.dasherize(self) + end + + def demodulize + Inflector.demodulize(self) + end + + def tableize + Inflector.tableize(self) + end + + def classify + Inflector.classify(self) + end + + # Capitalizes the first word and turns underscores into spaces and strips _id, so "employee_salary" becomes "Employee salary" + # and "author_id" becomes "Author". + def humanize + Inflector.humanize(self) + end + + def foreign_key(separate_class_name_and_id_with_underscore = true) + Inflector.foreign_key(self, separate_class_name_and_id_with_underscore) + end + + def constantize + Inflector.constantize(self) + end + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/string/iterators.rb b/vendor/rails/activesupport/lib/active_support/core_ext/string/iterators.rb new file mode 100644 index 00000000..73114d9d --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/string/iterators.rb @@ -0,0 +1,17 @@ +require 'strscan' + +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module String #:nodoc: + # Custom string iterators + module Iterators + # Yields a single-character string for each character in the string. + # When $KCODE = 'UTF8', multi-byte characters are yielded appropriately. + def each_char + scanner, char = StringScanner.new(self), /./mu + loop { yield(scanner.scan(char) || break) } + end + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/string/starts_ends_with.rb b/vendor/rails/activesupport/lib/active_support/core_ext/string/starts_ends_with.rb new file mode 100644 index 00000000..67174019 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/string/starts_ends_with.rb @@ -0,0 +1,20 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module String #:nodoc: + # Additional string tests. + module StartsEndsWith + # Does the string start with the specified +prefix+? + def starts_with?(prefix) + prefix = prefix.to_s + self[0, prefix.length] == prefix + end + + # Does the string end with the specified +suffix+? + def ends_with?(suffix) + suffix = suffix.to_s + self[-suffix.length, suffix.length] == suffix + end + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/symbol.rb b/vendor/rails/activesupport/lib/active_support/core_ext/symbol.rb new file mode 100644 index 00000000..3a412f64 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/symbol.rb @@ -0,0 +1,12 @@ +class Symbol + # Turns the symbol into a simple proc, which is especially useful for enumerations. Examples: + # + # # The same as people.collect { |p| p.name } + # people.collect(&:name) + # + # # The same as people.select { |p| p.manager? }.collect { |p| p.salary } + # people.select(&:manager?).collect(&:salary) + def to_proc + Proc.new { |obj, *args| obj.send(self, *args) } + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/time.rb b/vendor/rails/activesupport/lib/active_support/core_ext/time.rb new file mode 100644 index 00000000..9e3c7a03 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/time.rb @@ -0,0 +1,7 @@ +require File.dirname(__FILE__) + '/time/calculations' +require File.dirname(__FILE__) + '/time/conversions' + +class Time#:nodoc: + include ActiveSupport::CoreExtensions::Time::Calculations + include ActiveSupport::CoreExtensions::Time::Conversions +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/time/calculations.rb b/vendor/rails/activesupport/lib/active_support/core_ext/time/calculations.rb new file mode 100644 index 00000000..7c60d5be --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -0,0 +1,189 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Time #:nodoc: + # Enables the use of time calculations within Time itself + module Calculations + def self.append_features(base) #:nodoc: + super + base.extend(ClassMethods) + end + + module ClassMethods + # Return the number of days in the given month. If a year is given, + # February will return the correct number of days for leap years. + # Otherwise, this method will always report February as having 28 + # days. + def days_in_month(month, year=nil) + if month == 2 + !year.nil? && (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)) ? 29 : 28 + elsif month <= 7 + month % 2 == 0 ? 30 : 31 + else + month % 2 == 0 ? 31 : 30 + end + end + end + + # Seconds since midnight: Time.now.seconds_since_midnight + def seconds_since_midnight + self.hour.hours + self.min.minutes + self.sec + (self.usec/1.0e+6) + end + + # Returns a new Time where one or more of the elements have been changed according to the +options+ parameter. The time options + # (hour, minute, sec, usec) reset cascadingly, so if only the hour is passed, then minute, sec, and usec is set to 0. If the hour and + # minute is passed, then sec and usec is set to 0. + def change(options) + ::Time.send( + self.utc? ? :utc : :local, + options[:year] || self.year, + options[:month] || self.month, + options[:mday] || self.mday, + options[:hour] || self.hour, + options[:min] || (options[:hour] ? 0 : self.min), + options[:sec] || ((options[:hour] || options[:min]) ? 0 : self.sec), + options[:usec] || ((options[:hour] || options[:min] || options[:sec]) ? 0 : self.usec) + ) + end + + # Uses Date to provide precise Time calculations for years, months, and days. The +options+ parameter takes a hash with + # any of these keys: :months, :days, :years. + def advance(options) + d = ::Date.new(year + (options.delete(:years) || 0), month, day) + d = d >> options.delete(:months) if options[:months] + d = d + options.delete(:days) if options[:days] + change(options.merge(:year => d.year, :month => d.month, :mday => d.day)) + end + + # Returns a new Time representing the time a number of seconds ago, this is basically a wrapper around the Numeric extension + # Do not use this method in combination with x.months, use months_ago instead! + def ago(seconds) + seconds.until(self) + end + + # Returns a new Time representing the time a number of seconds since the instance time, this is basically a wrapper around + #the Numeric extension. Do not use this method in combination with x.months, use months_since instead! + def since(seconds) + seconds.since(self) + end + alias :in :since + + # Returns a new Time representing the time a number of specified months ago + def months_ago(months) + months_since(-months) + end + + def months_since(months) + year, month, mday = self.year, self.month, self.mday + + month += months + + # in case months is negative + while month < 1 + month += 12 + year -= 1 + end + + # in case months is positive + while month > 12 + month -= 12 + year += 1 + end + + max = ::Time.days_in_month(month, year) + mday = max if mday > max + + change(:year => year, :month => month, :mday => mday) + end + + # Returns a new Time representing the time a number of specified years ago + def years_ago(years) + change(:year => self.year - years) + end + + def years_since(years) + change(:year => self.year + years) + end + + # Short-hand for years_ago(1) + def last_year + years_ago(1) + end + + # Short-hand for years_since(1) + def next_year + years_since(1) + end + + + # Short-hand for months_ago(1) + def last_month + months_ago(1) + end + + # Short-hand for months_since(1) + def next_month + months_since(1) + end + + # Returns a new Time representing the "start" of this week (Monday, 0:00) + def beginning_of_week + days_to_monday = self.wday!=0 ? self.wday-1 : 6 + (self - days_to_monday.days).midnight + end + alias :monday :beginning_of_week + alias :at_beginning_of_week :beginning_of_week + + # Returns a new Time representing the start of the given day in next week (default is Monday). + def next_week(day = :monday) + days_into_week = { :monday => 0, :tuesday => 1, :wednesday => 2, :thursday => 3, :friday => 4, :saturday => 5, :sunday => 6} + since(1.week).beginning_of_week.since(days_into_week[day].day).change(:hour => 0) + end + + # Returns a new Time representing the start of the day (0:00) + def beginning_of_day + (self - self.seconds_since_midnight).change(:usec => 0) + end + alias :midnight :beginning_of_day + alias :at_midnight :beginning_of_day + alias :at_beginning_of_day :beginning_of_day + + # Returns a new Time representing the start of the month (1st of the month, 0:00) + def beginning_of_month + #self - ((self.mday-1).days + self.seconds_since_midnight) + change(:mday => 1,:hour => 0, :min => 0, :sec => 0, :usec => 0) + end + alias :at_beginning_of_month :beginning_of_month + + # Returns a new Time representing the end of the month (last day of the month, 0:00) + def end_of_month + #self - ((self.mday-1).days + self.seconds_since_midnight) + last_day = ::Time.days_in_month( self.month, self.year ) + change(:mday => last_day,:hour => 0, :min => 0, :sec => 0, :usec => 0) + end + alias :at_end_of_month :end_of_month + + # Returns a new Time representing the start of the quarter (1st of january, april, july, october, 0:00) + def beginning_of_quarter + beginning_of_month.change(:month => [10, 7, 4, 1].detect { |m| m <= self.month }) + end + alias :at_beginning_of_quarter :beginning_of_quarter + + # Returns a new Time representing the start of the year (1st of january, 0:00) + def beginning_of_year + change(:month => 1,:mday => 1,:hour => 0, :min => 0, :sec => 0, :usec => 0) + end + alias :at_beginning_of_year :beginning_of_year + + # Convenience method which returns a new Time representing the time 1 day ago + def yesterday + self.ago(1.day) + end + + # Convenience method which returns a new Time representing the time 1 day since the instance time + def tomorrow + self.since(1.day) + end + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/time/conversions.rb b/vendor/rails/activesupport/lib/active_support/core_ext/time/conversions.rb new file mode 100644 index 00000000..be8dc0e7 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -0,0 +1,37 @@ +require 'date' +require 'time' + +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Time #:nodoc: + # Getting times in different convenient string representations and other objects + module Conversions + DATE_FORMATS = { + :db => "%Y-%m-%d %H:%M:%S", + :short => "%d %b %H:%M", + :long => "%B %d, %Y %H:%M", + :rfc822 => "%a, %d %b %Y %H:%M:%S %z" + } + + def self.append_features(klass) + super + klass.send(:alias_method, :to_default_s, :to_s) + klass.send(:alias_method, :to_s, :to_formatted_s) + end + + def to_formatted_s(format = :default) + DATE_FORMATS[format] ? strftime(DATE_FORMATS[format]).strip : to_default_s + end + + def to_date + ::Date.new(year, month, day) + end + + # To be able to keep Dates and Times interchangeable on conversions + def to_time + self + end + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/dependencies.rb b/vendor/rails/activesupport/lib/active_support/dependencies.rb new file mode 100644 index 00000000..c1da53c8 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/dependencies.rb @@ -0,0 +1,173 @@ +require 'set' +require File.dirname(__FILE__) + '/core_ext/module/attribute_accessors' +require File.dirname(__FILE__) + '/core_ext/load_error' +require File.dirname(__FILE__) + '/core_ext/kernel' + +module Dependencies #:nodoc: + extend self + + # Should we turn on Ruby warnings on the first load of dependent files? + mattr_accessor :warnings_on_first_load + self.warnings_on_first_load = false + + # All files ever loaded. + mattr_accessor :history + self.history = Set.new + + # All files currently loaded. + mattr_accessor :loaded + self.loaded = Set.new + + # Should we load files or require them? + mattr_accessor :mechanism + self.mechanism = :load + + def load? + mechanism == :load + end + + def depend_on(file_name, swallow_load_errors = false) + require_or_load(file_name) + rescue LoadError + raise unless swallow_load_errors + end + + def associate_with(file_name) + depend_on(file_name, true) + end + + def clear + loaded.clear + end + + def require_or_load(file_name) + file_name = $1 if file_name =~ /^(.*)\.rb$/ + return if loaded.include?(file_name) + + # Record that we've seen this file *before* loading it to avoid an + # infinite loop with mutual dependencies. + loaded << file_name + + if load? + begin + # Enable warnings iff this file has not been loaded before and + # warnings_on_first_load is set. + if !warnings_on_first_load or history.include?(file_name) + load "#{file_name}.rb" + else + enable_warnings { load "#{file_name}.rb" } + end + rescue + loaded.delete file_name + raise + end + else + require file_name + end + + # Record history *after* loading so first load gets warnings. + history << file_name + end + + class LoadingModule + # Old style environment.rb referenced this method directly. Please note, it doesn't + # actualy *do* anything any more. + def self.root(*args) + if defined?(RAILS_DEFAULT_LOGGER) + RAILS_DEFAULT_LOGGER.warn "Your environment.rb uses the old syntax, it may not continue to work in future releases." + RAILS_DEFAULT_LOGGER.warn "For upgrade instructions please see: http://manuals.rubyonrails.com/read/book/19" + end + end + end +end + +Object.send(:define_method, :require_or_load) { |file_name| Dependencies.require_or_load(file_name) } unless Object.respond_to?(:require_or_load) +Object.send(:define_method, :require_dependency) { |file_name| Dependencies.depend_on(file_name) } unless Object.respond_to?(:require_dependency) +Object.send(:define_method, :require_association) { |file_name| Dependencies.associate_with(file_name) } unless Object.respond_to?(:require_association) + +class Module #:nodoc: + # Rename the original handler so we can chain it to the new one + alias :rails_original_const_missing :const_missing + + # Use const_missing to autoload associations so we don't have to + # require_association when using single-table inheritance. + def const_missing(class_id) + file_name = class_id.to_s.demodulize.underscore + file_path = as_load_path.empty? ? file_name : "#{as_load_path}/#{file_name}" + begin + require_dependency(file_path) + brief_name = self == Object ? '' : "#{name}::" + raise NameError.new("uninitialized constant #{brief_name}#{class_id}") unless const_defined?(class_id) + return const_get(class_id) + rescue MissingSourceFile => e + # Re-raise the error if it does not concern the file we were trying to load. + raise unless e.is_missing? file_path + + # Look for a directory in the load path that we ought to load. + if $LOAD_PATH.any? { |base| File.directory? "#{base}/#{file_path}" } + mod = Module.new + const_set class_id, mod # Create the new module + return mod + end + + # Attempt to access the name from the parent, unless we don't have a valid + # parent, or the constant is already defined in the parent. If the latter + # is the case, then we are being queried via self::class_id, and we should + # avoid returning the constant from the parent if possible. + if parent && parent != self && ! parents.any? { |p| p.const_defined?(class_id) } + suppress(NameError) do + return parent.send(:const_missing, class_id) + end + end + + raise NameError.new("uninitialized constant #{class_id}").copy_blame!(e) + end + end +end + +class Class + def const_missing(class_id) + if [Object, Kernel].include?(self) || parent == self + super + else + parent.send :const_missing, class_id + end + end +end + +class Object #:nodoc: + def load(file, *extras) + super(file, *extras) + rescue Object => exception + exception.blame_file! file + raise + end + + def require(file, *extras) + super(file, *extras) + rescue Object => exception + exception.blame_file! file + raise + end +end + +# Add file-blaming to exceptions +class Exception #:nodoc: + def blame_file!(file) + (@blamed_files ||= []).unshift file + end + + def blamed_files + @blamed_files ||= [] + end + + def describe_blame + return nil if blamed_files.empty? + "This error occured while loading the following files:\n #{blamed_files.join "\n "}" + end + + def copy_blame!(exc) + @blamed_files = exc.blamed_files.clone + self + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/inflections.rb b/vendor/rails/activesupport/lib/active_support/inflections.rb new file mode 100644 index 00000000..294a65c3 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/inflections.rb @@ -0,0 +1,53 @@ +Inflector.inflections do |inflect| + inflect.plural(/$/, 's') + inflect.plural(/s$/i, 's') + inflect.plural(/(ax|test)is$/i, '\1es') + inflect.plural(/(octop|vir)us$/i, '\1i') + inflect.plural(/(alias|status)$/i, '\1es') + inflect.plural(/(bu)s$/i, '\1ses') + inflect.plural(/(buffal|tomat)o$/i, '\1oes') + inflect.plural(/([ti])um$/i, '\1a') + inflect.plural(/sis$/i, 'ses') + inflect.plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves') + inflect.plural(/(hive)$/i, '\1s') + inflect.plural(/([^aeiouy]|qu)y$/i, '\1ies') + inflect.plural(/([^aeiouy]|qu)ies$/i, '\1y') + inflect.plural(/(x|ch|ss|sh)$/i, '\1es') + inflect.plural(/(matr|vert|ind)ix|ex$/i, '\1ices') + inflect.plural(/([m|l])ouse$/i, '\1ice') + inflect.plural(/^(ox)$/i, '\1en') + inflect.plural(/(quiz)$/i, '\1zes') + + inflect.singular(/s$/i, '') + inflect.singular(/(n)ews$/i, '\1ews') + inflect.singular(/([ti])a$/i, '\1um') + inflect.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i, '\1\2sis') + inflect.singular(/(^analy)ses$/i, '\1sis') + inflect.singular(/([^f])ves$/i, '\1fe') + inflect.singular(/(hive)s$/i, '\1') + inflect.singular(/(tive)s$/i, '\1') + inflect.singular(/([lr])ves$/i, '\1f') + inflect.singular(/([^aeiouy]|qu)ies$/i, '\1y') + inflect.singular(/(s)eries$/i, '\1eries') + inflect.singular(/(m)ovies$/i, '\1ovie') + inflect.singular(/(x|ch|ss|sh)es$/i, '\1') + inflect.singular(/([m|l])ice$/i, '\1ouse') + inflect.singular(/(bus)es$/i, '\1') + inflect.singular(/(o)es$/i, '\1') + inflect.singular(/(shoe)s$/i, '\1') + inflect.singular(/(cris|ax|test)es$/i, '\1is') + inflect.singular(/([octop|vir])i$/i, '\1us') + inflect.singular(/(alias|status)es$/i, '\1') + inflect.singular(/^(ox)en/i, '\1') + inflect.singular(/(vert|ind)ices$/i, '\1ex') + inflect.singular(/(matr)ices$/i, '\1ix') + inflect.singular(/(quiz)zes$/i, '\1') + + inflect.irregular('person', 'people') + inflect.irregular('man', 'men') + inflect.irregular('child', 'children') + inflect.irregular('sex', 'sexes') + inflect.irregular('move', 'moves') + + inflect.uncountable(%w(equipment information rice money species series fish sheep)) +end diff --git a/vendor/rails/activesupport/lib/active_support/inflector.rb b/vendor/rails/activesupport/lib/active_support/inflector.rb new file mode 100644 index 00000000..ce36d3ef --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/inflector.rb @@ -0,0 +1,178 @@ +require 'singleton' + +# The Inflector transforms words from singular to plural, class names to table names, modularized class names to ones without, +# and class names to foreign keys. The default inflections for pluralization, singularization, and uncountable words are kept +# in inflections.rb. +module Inflector + # A singleton instance of this class is yielded by Inflector.inflections, which can then be used to specify additional + # inflection rules. Examples: + # + # Inflector.inflections do |inflect| + # inflect.plural /^(ox)$/i, '\1\2en' + # inflect.singular /^(ox)en/i, '\1' + # + # inflect.irregular 'octopus', 'octopi' + # + # inflect.uncountable "equipment" + # end + # + # New rules are added at the top. So in the example above, the irregular rule for octopus will now be the first of the + # pluralization and singularization rules that is runs. This guarantees that your rules run before any of the rules that may + # already have been loaded. + class Inflections + include Singleton + + attr_reader :plurals, :singulars, :uncountables + + def initialize + @plurals, @singulars, @uncountables = [], [], [] + end + + # Specifies a new pluralization rule and its replacement. The rule can either be a string or a regular expression. + # The replacement should always be a string that may include references to the matched data from the rule. + def plural(rule, replacement) + @plurals.insert(0, [rule, replacement]) + end + + # Specifies a new singularization rule and its replacement. The rule can either be a string or a regular expression. + # The replacement should always be a string that may include references to the matched data from the rule. + def singular(rule, replacement) + @singulars.insert(0, [rule, replacement]) + end + + # Specifies a new irregular that applies to both pluralization and singularization at the same time. This can only be used + # for strings, not regular expressions. You simply pass the irregular in singular and plural form. + # + # Examples: + # irregular 'octopus', 'octopi' + # irregular 'person', 'people' + def irregular(singular, plural) + plural(Regexp.new("(#{singular[0,1]})#{singular[1..-1]}$", "i"), '\1' + plural[1..-1]) + singular(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + singular[1..-1]) + end + + # Add uncountable words that shouldn't be attempted inflected. + # + # Examples: + # uncountable "money" + # uncountable "money", "information" + # uncountable %w( money information rice ) + def uncountable(*words) + (@uncountables << words).flatten! + end + + # Clears the loaded inflections within a given scope (default is :all). Give the scope as a symbol of the inflection type, + # the options are: :plurals, :singulars, :uncountables + # + # Examples: + # clear :all + # clear :plurals + def clear(scope = :all) + case scope + when :all + @plurals, @singulars, @uncountables = [], [], [] + else + instance_variable_set "@#{scope}", [] + end + end + end + + extend self + + def inflections + if block_given? + yield Inflections.instance + else + Inflections.instance + end + end + + def pluralize(word) + result = word.to_s.dup + + if inflections.uncountables.include?(result.downcase) + result + else + inflections.plurals.each { |(rule, replacement)| break if result.gsub!(rule, replacement) } + result + end + end + + def singularize(word) + result = word.to_s.dup + + if inflections.uncountables.include?(result.downcase) + result + else + inflections.singulars.each { |(rule, replacement)| break if result.gsub!(rule, replacement) } + result + end + end + + def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true) + if first_letter_in_uppercase + lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase } + else + lower_case_and_underscored_word.first + camelize(lower_case_and_underscored_word)[1..-1] + end + end + + def titleize(word) + humanize(underscore(word)).gsub(/\b([a-z])/) { $1.capitalize } + end + + def underscore(camel_cased_word) + camel_cased_word.to_s.gsub(/::/, '/'). + gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). + gsub(/([a-z\d])([A-Z])/,'\1_\2'). + tr("-", "_"). + downcase + end + + def dasherize(underscored_word) + underscored_word.gsub(/_/, '-') + end + + def humanize(lower_case_and_underscored_word) + lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize + end + + def demodulize(class_name_in_module) + class_name_in_module.to_s.gsub(/^.*::/, '') + end + + def tableize(class_name) + pluralize(underscore(class_name)) + end + + def classify(table_name) + camelize(singularize(table_name)) + end + + def foreign_key(class_name, separate_class_name_and_id_with_underscore = true) + underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id") + end + + def constantize(camel_cased_word) + raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!" unless + /^(::)?([A-Z]\w*)(::[A-Z]\w*)*$/ =~ camel_cased_word + + camel_cased_word = "::#{camel_cased_word}" unless $1 + Object.module_eval(camel_cased_word, __FILE__, __LINE__) + end + + def ordinalize(number) + if (11..13).include?(number.to_i % 100) + "#{number}th" + else + case number.to_i % 10 + when 1: "#{number}st" + when 2: "#{number}nd" + when 3: "#{number}rd" + else "#{number}th" + end + end + end +end + +require File.dirname(__FILE__) + '/inflections' diff --git a/vendor/rails/activesupport/lib/active_support/json.rb b/vendor/rails/activesupport/lib/active_support/json.rb new file mode 100644 index 00000000..7d5304dd --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/json.rb @@ -0,0 +1,37 @@ +require 'active_support/json/encoders' + +module ActiveSupport + module JSON #:nodoc: + class CircularReferenceError < StandardError #:nodoc: + end + # returns the literal string as its JSON encoded form. Useful for passing javascript variables into functions. + # + # page.call 'Element.show', ActiveSupport::JSON::Variable.new("$$(#items li)") + class Variable < String #:nodoc: + def to_json + self + end + end + + class << self + REFERENCE_STACK_VARIABLE = :json_reference_stack + + def encode(value) + raise_on_circular_reference(value) do + Encoders[value.class].call(value) + end + end + + protected + def raise_on_circular_reference(value) + stack = Thread.current[REFERENCE_STACK_VARIABLE] ||= [] + raise CircularReferenceError, 'object references itself' if + stack.include? value + stack << value + yield + ensure + stack.pop + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/json/encoders.rb b/vendor/rails/activesupport/lib/active_support/json/encoders.rb new file mode 100644 index 00000000..c3e3619f --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/json/encoders.rb @@ -0,0 +1,25 @@ +module ActiveSupport + module JSON #:nodoc: + module Encoders + mattr_accessor :encoders + @@encoders = {} + + class << self + def define_encoder(klass, &block) + encoders[klass] = block + end + + def [](klass) + klass.ancestors.each do |k| + encoder = encoders[k] + return encoder if encoder + end + end + end + end + end +end + +Dir[File.dirname(__FILE__) + '/encoders/*.rb'].each do |file| + require file[0..-4] +end diff --git a/vendor/rails/activesupport/lib/active_support/json/encoders/core.rb b/vendor/rails/activesupport/lib/active_support/json/encoders/core.rb new file mode 100644 index 00000000..97f994ac --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/json/encoders/core.rb @@ -0,0 +1,65 @@ +module ActiveSupport + module JSON #:nodoc: + module Encoders #:nodoc: + define_encoder Object do |object| + object.instance_values.to_json + end + + define_encoder TrueClass do + 'true' + end + + define_encoder FalseClass do + 'false' + end + + define_encoder NilClass do + 'null' + end + + define_encoder String do |string| + returning value = '"' do + string.each_char do |char| + value << case + when char == "\010": '\b' + when char == "\f": '\f' + when char == "\n": '\n' + when char == "\r": '\r' + when char == "\t": '\t' + when char == '"': '\"' + when char == '\\': '\\\\' + when char.length > 1: "\\u#{'%04x' % char.unpack('U').first}" + else; char + end + end + value << '"' + end + end + + define_encoder Numeric do |numeric| + numeric.to_s + end + + define_encoder Symbol do |symbol| + symbol.to_s.to_json + end + + define_encoder Enumerable do |enumerable| + "[#{enumerable.map { |value| value.to_json } * ', '}]" + end + + define_encoder Hash do |hash| + returning result = '{' do + result << hash.map do |pair| + pair.map { |value| value.to_json } * ': ' + end * ', ' + result << '}' + end + end + + define_encoder Regexp do |regexp| + regexp.inspect + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/option_merger.rb b/vendor/rails/activesupport/lib/active_support/option_merger.rb new file mode 100644 index 00000000..51a2ea13 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/option_merger.rb @@ -0,0 +1,25 @@ +module ActiveSupport + class OptionMerger #:nodoc: + instance_methods.each do |method| + undef_method(method) if method !~ /^(__|instance_eval)/ + end + + def initialize(context, options) + @context, @options = context, options + end + + private + def method_missing(method, *arguments, &block) + merge_argument_options! arguments + @context.send(method, *arguments, &block) + end + + def merge_argument_options!(arguments) + arguments << if arguments.last.respond_to? :merge! + arguments.pop.dup.merge!(@options) + else + @options.dup + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/ordered_options.rb b/vendor/rails/activesupport/lib/active_support/ordered_options.rb new file mode 100644 index 00000000..0e97578b --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/ordered_options.rb @@ -0,0 +1,43 @@ +class OrderedHash < Array #:nodoc: + def []=(key, value) + if pair = find_pair(key) + pair.pop + pair << value + else + self << [key, value] + end + end + + def [](key) + pair = find_pair(key) + pair ? pair.last : nil + end + + def keys + self.collect { |i| i.first } + end + + private + def find_pair(key) + self.each { |i| return i if i.first == key } + return false + end +end + +class OrderedOptions < OrderedHash #:nodoc: + def []=(key, value) + super(key.to_sym, value) + end + + def [](key) + super(key.to_sym) + end + + def method_missing(name, *args) + if name.to_s =~ /(.*)=$/ + self[$1.to_sym] = args.first + else + self[name] + end + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/reloadable.rb b/vendor/rails/activesupport/lib/active_support/reloadable.rb new file mode 100644 index 00000000..3fd13e3d --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/reloadable.rb @@ -0,0 +1,30 @@ +# Classes that include this module will automatically be reloaded +# by the Rails dispatcher when Dependencies.mechanism = :load. +module Reloadable + class << self + def included(base) #nodoc: + raise TypeError, "Only Classes can be Reloadable!" unless base.is_a? Class + + unless base.respond_to?(:reloadable?) + class << base + define_method(:reloadable?) { true } + end + end + end + + def reloadable_classes + included_in_classes.select { |klass| klass.reloadable? } + end + end + + # Captures the common pattern where a base class should not be reloaded, + # but its subclasses should be. + module Subclasses + def self.included(base) #nodoc: + base.send :include, Reloadable + (class << base; self; end).send(:define_method, :reloadable?) do + base != self + end + end + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/values/time_zone.rb b/vendor/rails/activesupport/lib/active_support/values/time_zone.rb new file mode 100644 index 00000000..3aa135c2 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/values/time_zone.rb @@ -0,0 +1,180 @@ +# A value object representing a time zone. A time zone is simply a named +# offset (in seconds) from GMT. Note that two time zone objects are only +# equivalent if they have both the same offset, and the same name. +# +# A TimeZone instance may be used to convert a Time value to the corresponding +# time zone. +# +# The class also includes #all, which returns a list of all TimeZone objects. +class TimeZone + include Comparable + + attr_reader :name, :utc_offset + + # Create a new TimeZone object with the given name and offset. The offset is + # the number of seconds that this time zone is offset from UTC (GMT). Seconds + # were chosen as the offset unit because that is the unit that Ruby uses + # to represent time zone offsets (see Time#utc_offset). + def initialize(name, utc_offset) + @name = name + @utc_offset = utc_offset + end + + # Returns the offset of this time zone as a formatted string, of the + # format "+HH:MM". If the offset is zero, this returns the empty + # string. If +colon+ is false, a colon will not be inserted into the + # result. + def formatted_offset( colon=true ) + return "" if utc_offset == 0 + sign = (utc_offset < 0 ? -1 : 1) + hours = utc_offset.abs / 3600 + minutes = (utc_offset.abs % 3600) / 60 + "%+03d%s%02d" % [ hours * sign, colon ? ":" : "", minutes ] + end + + # Compute and return the current time, in the time zone represented by + # +self+. + def now + adjust(Time.now) + end + + # Return the current date in this time zone. + def today + now.to_date + end + + # Adjust the given time to the time zone represented by +self+. + def adjust(time) + time = time.to_time + time + utc_offset - time.utc_offset + end + + # Reinterprets the given time value as a time in the current time + # zone, and then adjusts it to return the corresponding time in the + # local time zone. + def unadjust(time) + time = Time.local(*time.to_time.to_a) + time - utc_offset + time.utc_offset + end + + # Compare this time zone to the parameter. The two are comapred first on + # their offsets, and then by name. + def <=>(zone) + result = (utc_offset <=> zone.utc_offset) + result = (name <=> zone.name) if result == 0 + result + end + + # Returns a textual representation of this time zone. + def to_s + "(GMT#{formatted_offset}) #{name}" + end + + @@zones = nil + + class << self + # Create a new TimeZone instance with the given name and offset. + def create(name, offset) + zone = allocate + zone.send :initialize, name, offset + zone + end + + # Return a TimeZone instance with the given name, or +nil+ if no + # such TimeZone instance exists. (This exists to support the use of + # this class with the #composed_of macro.) + def new(name) + self[name] + end + + # Return an array of all TimeZone objects. There are multiple TimeZone + # objects per time zone, in many cases, to make it easier for users to + # find their own time zone. + def all + unless @@zones + @@zones = [] + [[-43_200, "International Date Line West" ], + [-39_600, "Midway Island", "Samoa" ], + [-36_000, "Hawaii" ], + [-32_400, "Alaska" ], + [-28_800, "Pacific Time (US & Canada)", "Tijuana" ], + [-25_200, "Mountain Time (US & Canada)", "Chihuahua", "La Paz", + "Mazatlan", "Arizona" ], + [-21_600, "Central Time (US & Canada)", "Saskatchewan", "Guadalajara", + "Mexico City", "Monterrey", "Central America" ], + [-18_000, "Eastern Time (US & Canada)", "Indiana (East)", "Bogota", + "Lima", "Quito" ], + [-14_400, "Atlantic Time (Canada)", "Caracas", "La Paz", "Santiago" ], + [-12_600, "Newfoundland" ], + [-10_800, "Brasilia", "Buenos Aires", "Georgetown", "Greenland" ], + [ -7_200, "Mid-Atlantic" ], + [ -3_600, "Azores", "Cape Verde Is." ], + [ 0, "Dublin", "Edinburgh", "Lisbon", "London", "Casablanca", + "Monrovia" ], + [ 3_600, "Belgrade", "Bratislava", "Budapest", "Ljubljana", "Prague", + "Sarajevo", "Skopje", "Warsaw", "Zagreb", "Brussels", + "Copenhagen", "Madrid", "Paris", "Amsterdam", "Berlin", + "Bern", "Rome", "Stockholm", "Vienna", + "West Central Africa" ], + [ 7_200, "Bucharest", "Cairo", "Helsinki", "Kyev", "Riga", "Sofia", + "Tallinn", "Vilnius", "Athens", "Istanbul", "Minsk", + "Jerusalem", "Harare", "Pretoria" ], + [ 10_800, "Moscow", "St. Petersburg", "Volgograd", "Kuwait", "Riyadh", + "Nairobi", "Baghdad" ], + [ 12_600, "Tehran" ], + [ 14_400, "Abu Dhabi", "Muscat", "Baku", "Tbilisi", "Yerevan" ], + [ 16_200, "Kabul" ], + [ 18_000, "Ekaterinburg", "Islamabad", "Karachi", "Tashkent" ], + [ 19_800, "Chennai", "Kolkata", "Mumbai", "New Delhi" ], + [ 20_700, "Kathmandu" ], + [ 21_600, "Astana", "Dhaka", "Sri Jayawardenepura", "Almaty", + "Novosibirsk" ], + [ 23_400, "Rangoon" ], + [ 25_200, "Bangkok", "Hanoi", "Jakarta", "Krasnoyarsk" ], + [ 28_800, "Beijing", "Chongqing", "Hong Kong", "Urumqi", + "Kuala Lumpur", "Singapore", "Taipei", "Perth", "Irkutsk", + "Ulaan Bataar" ], + [ 32_400, "Seoul", "Osaka", "Sapporo", "Tokyo", "Yakutsk" ], + [ 34_200, "Darwin", "Adelaide" ], + [ 36_000, "Canberra", "Melbourne", "Sydney", "Brisbane", "Hobart", + "Vladivostok", "Guam", "Port Moresby" ], + [ 39_600, "Magadan", "Solomon Is.", "New Caledonia" ], + [ 43_200, "Fiji", "Kamchatka", "Marshall Is.", "Auckland", + "Wellington" ], + [ 46_800, "Nuku'alofa" ]]. + each do |offset, *places| + places.each { |place| @@zones << create(place, offset).freeze } + end + @@zones.sort! + end + @@zones + end + + # Locate a specific time zone object. If the argument is a string, it + # is interpreted to mean the name of the timezone to locate. If it is a + # numeric value it is either the hour offset, or the second offset, of the + # timezone to find. (The first one with that offset will be returned.) + # Returns +nil+ if no such time zone is known to the system. + def [](arg) + case arg + when String + all.find { |z| z.name == arg } + when Numeric + arg *= 3600 if arg.abs <= 13 + all.find { |z| z.utc_offset == arg.to_i } + else + raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}" + end + end + + # A regular expression that matches the names of all time zones in + # the USA. + US_ZONES = /US|Arizona|Indiana|Hawaii|Alaska/ + + # A convenience method for returning a collection of TimeZone objects + # for time zones in the USA. + def us_zones + all.find_all { |z| z.name =~ US_ZONES } + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/vendor/builder.rb b/vendor/rails/activesupport/lib/active_support/vendor/builder.rb new file mode 100644 index 00000000..97192776 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/vendor/builder.rb @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby + +#-- +# Copyright 2004 by Jim Weirich (jim@weirichhouse.org). +# All rights reserved. + +# Permission is granted for use, copying, modification, distribution, +# and distribution of modified versions of this work as long as the +# above copyright notice is included. +#++ + +require 'builder/xmlmarkup' +require 'builder/xmlevents' diff --git a/vendor/rails/activesupport/lib/active_support/vendor/builder/blankslate.rb b/vendor/rails/activesupport/lib/active_support/vendor/builder/blankslate.rb new file mode 100644 index 00000000..1408c872 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/vendor/builder/blankslate.rb @@ -0,0 +1,53 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2004 by Jim Weirich (jim@weirichhouse.org). +# All rights reserved. + +# Permission is granted for use, copying, modification, distribution, +# and distribution of modified versions of this work as long as the +# above copyright notice is included. +#++ + +module Builder #:nodoc: + + # BlankSlate provides an abstract base class with no predefined + # methods (except for \_\_send__ and \_\_id__). + # BlankSlate is useful as a base class when writing classes that + # depend upon method_missing (e.g. dynamic proxies). + class BlankSlate #:nodoc: + class << self + def hide(name) + undef_method name if + instance_methods.include?(name.to_s) and + name !~ /^(__|instance_eval)/ + end + end + + instance_methods.each { |m| hide(m) } + end +end + +# Since Ruby is very dynamic, methods added to the ancestors of +# BlankSlate after BlankSlate is defined will show up in the +# list of available BlankSlate methods. We handle this by defining a hook in the Object and Kernel classes that will hide any defined +module Kernel #:nodoc: + class << self + alias_method :blank_slate_method_added, :method_added + def method_added(name) + blank_slate_method_added(name) + return if self != Kernel + Builder::BlankSlate.hide(name) + end + end +end + +class Object #:nodoc: + class << self + alias_method :blank_slate_method_added, :method_added + def method_added(name) + blank_slate_method_added(name) + return if self != Object + Builder::BlankSlate.hide(name) + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/vendor/builder/xmlbase.rb b/vendor/rails/activesupport/lib/active_support/vendor/builder/xmlbase.rb new file mode 100644 index 00000000..7202bb2e --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/vendor/builder/xmlbase.rb @@ -0,0 +1,143 @@ +#!/usr/bin/env ruby + +require 'builder/blankslate' + +module Builder #:nodoc: + + # Generic error for builder + class IllegalBlockError < RuntimeError #:nodoc: + end + + # XmlBase is a base class for building XML builders. See + # Builder::XmlMarkup and Builder::XmlEvents for examples. + class XmlBase < BlankSlate #:nodoc: + + # Create an XML markup builder. + # + # out:: Object receiving the markup.1 +out+ must respond to + # <<. + # indent:: Number of spaces used for indentation (0 implies no + # indentation and no line breaks). + # initial:: Level of initial indentation. + # + def initialize(indent=0, initial=0) + @indent = indent + @level = initial + end + + # Create a tag named +sym+. Other than the first argument which + # is the tag name, the arguements are the same as the tags + # implemented via method_missing. + def tag!(sym, *args, &block) + self.__send__(sym, *args, &block) + end + + # Create XML markup based on the name of the method. This method + # is never invoked directly, but is called for each markup method + # in the markup block. + def method_missing(sym, *args, &block) + text = nil + attrs = nil + sym = "#{sym}:#{args.shift}" if args.first.kind_of?(Symbol) + args.each do |arg| + case arg + when Hash + attrs ||= {} + attrs.merge!(arg) + else + text ||= '' + text << arg.to_s + end + end + if block + unless text.nil? + raise ArgumentError, "XmlMarkup cannot mix a text argument with a block" + end + _capture_outer_self(block) if @self.nil? + _indent + _start_tag(sym, attrs) + _newline + _nested_structures(block) + _indent + _end_tag(sym) + _newline + elsif text.nil? + _indent + _start_tag(sym, attrs, true) + _newline + else + _indent + _start_tag(sym, attrs) + text! text + _end_tag(sym) + _newline + end + @target + end + + # Append text to the output target. Escape any markup. May be + # used within the markup brackets as: + # + # builder.p { br; text! "HI" } #=>


      HI

      + def text!(text) + _text(_escape(text)) + end + + # Append text to the output target without escaping any markup. + # May be used within the markup brackets as: + # + # builder.p { |x| x << "
      HI" } #=>


      HI

      + # + # This is useful when using non-builder enabled software that + # generates strings. Just insert the string directly into the + # builder without changing the inserted markup. + # + # It is also useful for stacking builder objects. Builders only + # use << to append to the target, so by supporting this + # method/operation builders can use other builders as their + # targets. + def <<(text) + _text(text) + end + + # For some reason, nil? is sent to the XmlMarkup object. If nil? + # is not defined and method_missing is invoked, some strange kind + # of recursion happens. Since nil? won't ever be an XML tag, it + # is pretty safe to define it here. (Note: this is an example of + # cargo cult programming, + # cf. http://fishbowl.pastiche.org/2004/10/13/cargo_cult_programming). + def nil? + false + end + + private + + def _escape(text) + text. + gsub(%r{&}, '&'). + gsub(%r{<}, '<'). + gsub(%r{>}, '>') + end + + def _capture_outer_self(block) + @self = eval("self", block) + end + + def _newline + return if @indent == 0 + text! "\n" + end + + def _indent + return if @indent == 0 || @level == 0 + text!(" " * (@level * @indent)) + end + + def _nested_structures(block) + @level += 1 + block.call(self) + ensure + @level -= 1 + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/vendor/builder/xmlevents.rb b/vendor/rails/activesupport/lib/active_support/vendor/builder/xmlevents.rb new file mode 100644 index 00000000..15dc7b64 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/vendor/builder/xmlevents.rb @@ -0,0 +1,63 @@ +#!/usr/bin/env ruby + +#-- +# Copyright 2004 by Jim Weirich (jim@weirichhouse.org). +# All rights reserved. + +# Permission is granted for use, copying, modification, distribution, +# and distribution of modified versions of this work as long as the +# above copyright notice is included. +#++ + +require 'builder/xmlmarkup' + +module Builder + + # Create a series of SAX-like XML events (e.g. start_tag, end_tag) + # from the markup code. XmlEvent objects are used in a way similar + # to XmlMarkup objects, except that a series of events are generated + # and passed to a handler rather than generating character-based + # markup. + # + # Usage: + # xe = Builder::XmlEvents.new(hander) + # xe.title("HI") # Sends start_tag/end_tag/text messages to the handler. + # + # Indentation may also be selected by providing value for the + # indentation size and initial indentation level. + # + # xe = Builder::XmlEvents.new(handler, indent_size, initial_indent_level) + # + # == XML Event Handler + # + # The handler object must expect the following events. + # + # [start_tag(tag, attrs)] + # Announces that a new tag has been found. +tag+ is the name of + # the tag and +attrs+ is a hash of attributes for the tag. + # + # [end_tag(tag)] + # Announces that an end tag for +tag+ has been found. + # + # [text(text)] + # Announces that a string of characters (+text+) has been found. + # A series of characters may be broken up into more than one + # +text+ call, so the client cannot assume that a single + # callback contains all the text data. + # + class XmlEvents < XmlMarkup #:nodoc: + def text!(text) + @target.text(text) + end + + def _start_tag(sym, attrs, end_too=false) + @target.start_tag(sym, attrs) + _end_tag(sym) if end_too + end + + def _end_tag(sym) + @target.end_tag(sym) + end + end + +end diff --git a/vendor/rails/activesupport/lib/active_support/vendor/builder/xmlmarkup.rb b/vendor/rails/activesupport/lib/active_support/vendor/builder/xmlmarkup.rb new file mode 100644 index 00000000..b7e3b2d0 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/vendor/builder/xmlmarkup.rb @@ -0,0 +1,308 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2004 by Jim Weirich (jim@weirichhouse.org). +# All rights reserved. + +# Permission is granted for use, copying, modification, distribution, +# and distribution of modified versions of this work as long as the +# above copyright notice is included. +#++ + +# Provide a flexible and easy to use Builder for creating XML markup. +# See XmlBuilder for usage details. + +require 'builder/xmlbase' + +module Builder + + # Create XML markup easily. All (well, almost all) methods sent to + # an XmlMarkup object will be translated to the equivalent XML + # markup. Any method with a block will be treated as an XML markup + # tag with nested markup in the block. + # + # Examples will demonstrate this easier than words. In the + # following, +xm+ is an +XmlMarkup+ object. + # + # xm.em("emphasized") # => emphasized + # xm.em { xmm.b("emp & bold") } # => emph & bold + # xm.a("A Link", "href"=>"http://onestepback.org") + # # => A Link + # xm.div { br } # =>

      + # xm.target("name"=>"compile", "option"=>"fast") + # # => + # # NOTE: order of attributes is not specified. + # + # xm.instruct! # + # xm.html { # + # xm.head { # + # xm.title("History") # History + # } # + # xm.body { # + # xm.comment! "HI" # + # xm.h1("Header") #

      Header

      + # xm.p("paragraph") #

      paragraph

      + # } # + # } # + # + # == Notes: + # + # * The order that attributes are inserted in markup tags is + # undefined. + # + # * Sometimes you wish to insert text without enclosing tags. Use + # the text! method to accomplish this. + # + # Example: + # + # xm.div { #
      + # xm.text! "line"; xm.br # line
      + # xm.text! "another line"; xmbr # another line
      + # } #
      + # + # * The special XML characters <, >, and & are converted to <, + # > and & automatically. Use the << operation to + # insert text without modification. + # + # * Sometimes tags use special characters not allowed in ruby + # identifiers. Use the tag! method to handle these + # cases. + # + # Example: + # + # xml.tag!("SOAP:Envelope") { ... } + # + # will produce ... + # + # ... " + # + # tag! will also take text and attribute arguments (after + # the tag name) like normal markup methods. (But see the next + # bullet item for a better way to handle XML namespaces). + # + # * Direct support for XML namespaces is now available. If the + # first argument to a tag call is a symbol, it will be joined to + # the tag to produce a namespace:tag combination. It is easier to + # show this than describe it. + # + # xml.SOAP :Envelope do ... end + # + # Just put a space before the colon in a namespace to produce the + # right form for builder (e.g. "SOAP:Envelope" => + # "xml.SOAP :Envelope") + # + # * XmlMarkup builds the markup in any object (called a _target_) + # that accepts the << method. If no target is given, + # then XmlMarkup defaults to a string target. + # + # Examples: + # + # xm = Builder::XmlMarkup.new + # result = xm.title("yada") + # # result is a string containing the markup. + # + # buffer = "" + # xm = Builder::XmlMarkup.new(buffer) + # # The markup is appended to buffer (using <<) + # + # xm = Builder::XmlMarkup.new(STDOUT) + # # The markup is written to STDOUT (using <<) + # + # xm = Builder::XmlMarkup.new + # x2 = Builder::XmlMarkup.new(:target=>xm) + # # Markup written to +x2+ will be send to +xm+. + # + # * Indentation is enabled by providing the number of spaces to + # indent for each level as a second argument to XmlBuilder.new. + # Initial indentation may be specified using a third parameter. + # + # Example: + # + # xm = Builder.new(:ident=>2) + # # xm will produce nicely formatted and indented XML. + # + # xm = Builder.new(:indent=>2, :margin=>4) + # # xm will produce nicely formatted and indented XML with 2 + # # spaces per indent and an over all indentation level of 4. + # + # builder = Builder::XmlMarkup.new(:target=>$stdout, :indent=>2) + # builder.name { |b| b.first("Jim"); b.last("Weirich) } + # # prints: + # # + # # Jim + # # Weirich + # # + # + # * The instance_eval implementation which forces self to refer to + # the message receiver as self is now obsolete. We now use normal + # block calls to execute the markup block. This means that all + # markup methods must now be explicitly send to the xml builder. + # For instance, instead of + # + # xml.div { strong("text") } + # + # you need to write: + # + # xml.div { xml.strong("text") } + # + # Although more verbose, the subtle change in semantics within the + # block was found to be prone to error. To make this change a + # little less cumbersome, the markup block now gets the markup + # object sent as an argument, allowing you to use a shorter alias + # within the block. + # + # For example: + # + # xml_builder = Builder::XmlMarkup.new + # xml_builder.div { |xml| + # xml.stong("text") + # } + # + class XmlMarkup < XmlBase + + # Create an XML markup builder. Parameters are specified by an + # option hash. + # + # :target=>target_object:: + # Object receiving the markup. +out+ must respond to the + # << operator. The default is a plain string target. + # :indent=>indentation:: + # Number of spaces used for indentation. The default is no + # indentation and no line breaks. + # :margin=>initial_indentation_level:: + # Amount of initial indentation (specified in levels, not + # spaces). + # + def initialize(options={}) + indent = options[:indent] || 0 + margin = options[:margin] || 0 + super(indent, margin) + @target = options[:target] || "" + end + + # Return the target of the builder. + def target! + @target + end + + def comment!(comment_text) + _ensure_no_block block_given? + _special("", comment_text, nil) + end + + # Insert an XML declaration into the XML markup. + # + # For example: + # + # xml.declare! :ELEMENT, :blah, "yada" + # # => + def declare!(inst, *args, &block) + _indent + @target << "" + _newline + end + + # Insert a processing instruction into the XML markup. E.g. + # + # For example: + # + # xml.instruct! + # #=> + # xml.instruct! :aaa, :bbb=>"ccc" + # #=> + # + def instruct!(directive_tag=:xml, attrs={}) + _ensure_no_block block_given? + if directive_tag == :xml + a = { :version=>"1.0", :encoding=>"UTF-8" } + attrs = a.merge attrs + end + _special( + "", + nil, + attrs, + [:version, :encoding, :standalone]) + end + + # Surrounds the given text with a CDATA tag + # + # For example: + # + # xml.cdata! "blah blah blah" + # # => + def cdata!(text) + _ensure_no_block block_given? + _special("", text, nil) + end + + private + + # NOTE: All private methods of a builder object are prefixed when + # a "_" character to avoid possible conflict with XML tag names. + + # Insert text directly in to the builder's target. + def _text(text) + @target << text + end + + # Insert special instruction. + def _special(open, close, data=nil, attrs=nil, order=[]) + _indent + @target << open + @target << data if data + _insert_attributes(attrs, order) if attrs + @target << close + _newline + end + + # Start an XML tag. If end_too is true, then the start + # tag is also the end tag (e.g.
      + def _start_tag(sym, attrs, end_too=false) + @target << "<#{sym}" + _insert_attributes(attrs) + @target << "/" if end_too + @target << ">" + end + + # Insert an ending tag. + def _end_tag(sym) + @target << "" + end + + # Insert the attributes (given in the hash). + def _insert_attributes(attrs, order=[]) + return if attrs.nil? + order.each do |k| + v = attrs[k] + @target << %{ #{k}="#{v}"} if v + end + attrs.each do |k, v| + @target << %{ #{k}="#{v}"} unless order.member?(k) + end + end + + def _ensure_no_block(got_block) + if got_block + fail IllegalBlockError, + "Blocks are not allowed on XML instructions" + end + end + + end + +end diff --git a/vendor/rails/activesupport/lib/active_support/version.rb b/vendor/rails/activesupport/lib/active_support/version.rb new file mode 100644 index 00000000..8e0ef729 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/version.rb @@ -0,0 +1,9 @@ +module ActiveSupport + module VERSION #:nodoc: + MAJOR = 1 + MINOR = 3 + TINY = 1 + + STRING = [MAJOR, MINOR, TINY].join('.') + end +end diff --git a/vendor/rails/activesupport/lib/active_support/whiny_nil.rb b/vendor/rails/activesupport/lib/active_support/whiny_nil.rb new file mode 100644 index 00000000..61cbe98e --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/whiny_nil.rb @@ -0,0 +1,38 @@ +# Extensions to nil which allow for more helpful error messages for +# people who are new to rails. +# +# The aim is to ensure that when users pass nil to methods where that isn't +# appropriate, instead of NoMethodError and the name of some method used +# by the framework users will see a message explaining what type of object +# was expected. + +class NilClass + WHINERS = [ ::ActiveRecord::Base, ::Array ] + + @@method_class_map = Hash.new + + WHINERS.each do |klass| + methods = klass.public_instance_methods - public_instance_methods + methods.each do |method| + @@method_class_map[method.to_sym] = klass + end + end + + def id + raise RuntimeError, "Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id", caller + end + + private + def method_missing(method, *args, &block) + raise_nil_warning_for @@method_class_map[method], method, caller + end + + def raise_nil_warning_for(klass = nil, selector = nil, with_caller = nil) + message = "You have a nil object when you didn't expect it!" + message << "\nYou might have expected an instance of #{klass}." if klass + message << "\nThe error occured while evaluating nil.#{selector}" if selector + + raise NoMethodError, message, with_caller || caller + end +end + diff --git a/vendor/rails/activesupport/test/autoloading_fixtures/a/b.rb b/vendor/rails/activesupport/test/autoloading_fixtures/a/b.rb new file mode 100644 index 00000000..9c9e6454 --- /dev/null +++ b/vendor/rails/activesupport/test/autoloading_fixtures/a/b.rb @@ -0,0 +1,2 @@ +class A::B +end \ No newline at end of file diff --git a/vendor/rails/activesupport/test/autoloading_fixtures/a/c/d.rb b/vendor/rails/activesupport/test/autoloading_fixtures/a/c/d.rb new file mode 100644 index 00000000..0f40d6fb --- /dev/null +++ b/vendor/rails/activesupport/test/autoloading_fixtures/a/c/d.rb @@ -0,0 +1,2 @@ +class A::C::D +end \ No newline at end of file diff --git a/vendor/rails/activesupport/test/autoloading_fixtures/a/c/e/f.rb b/vendor/rails/activesupport/test/autoloading_fixtures/a/c/e/f.rb new file mode 100644 index 00000000..57dba5a3 --- /dev/null +++ b/vendor/rails/activesupport/test/autoloading_fixtures/a/c/e/f.rb @@ -0,0 +1,2 @@ +class A::C::E::F +end \ No newline at end of file diff --git a/vendor/rails/activesupport/test/autoloading_fixtures/e.rb b/vendor/rails/activesupport/test/autoloading_fixtures/e.rb new file mode 100644 index 00000000..2f59e4fb --- /dev/null +++ b/vendor/rails/activesupport/test/autoloading_fixtures/e.rb @@ -0,0 +1,2 @@ +class E +end \ No newline at end of file diff --git a/vendor/rails/activesupport/test/autoloading_fixtures/module_folder/nested_class.rb b/vendor/rails/activesupport/test/autoloading_fixtures/module_folder/nested_class.rb new file mode 100644 index 00000000..39ee0e50 --- /dev/null +++ b/vendor/rails/activesupport/test/autoloading_fixtures/module_folder/nested_class.rb @@ -0,0 +1,2 @@ +class ModuleFolder::NestedClass +end \ No newline at end of file diff --git a/vendor/rails/activesupport/test/autoloading_fixtures/module_folder/nested_sibling.rb b/vendor/rails/activesupport/test/autoloading_fixtures/module_folder/nested_sibling.rb new file mode 100644 index 00000000..80244b8b --- /dev/null +++ b/vendor/rails/activesupport/test/autoloading_fixtures/module_folder/nested_sibling.rb @@ -0,0 +1,2 @@ +class ModuleFolder::NestedSibling +end \ No newline at end of file diff --git a/vendor/rails/activesupport/test/caching_tools_test.rb b/vendor/rails/activesupport/test/caching_tools_test.rb new file mode 100644 index 00000000..9f3e42e4 --- /dev/null +++ b/vendor/rails/activesupport/test/caching_tools_test.rb @@ -0,0 +1,81 @@ +require 'test/unit' +require File.dirname(__FILE__)+'/../lib/active_support/caching_tools' + +class HashCachingTests < Test::Unit::TestCase + + def cached(&proc) + return @cached if @cached + + @cached_class = Class.new(&proc) + @cached_class.class_eval do + extend ActiveSupport::CachingTools::HashCaching + hash_cache :slow_method + end + @cached = @cached_class.new + end + + def test_cache_access_should_call_method + cached do + def slow_method(a) raise "I should be here: #{a}"; end + end + assert_raises(RuntimeError) { cached.slow_method_cache[1] } + end + + def test_cache_access_should_actually_cache + cached do + def slow_method(a) + (@x ||= []) + if @x.include?(a) then raise "Called twice for #{a}!" + else + @x << a + a + 1 + end + end + end + assert_equal 11, cached.slow_method_cache[10] + assert_equal 12, cached.slow_method_cache[11] + assert_equal 11, cached.slow_method_cache[10] + assert_equal 12, cached.slow_method_cache[11] + end + + def test_cache_should_be_clearable + cached do + def slow_method(a) + @x ||= 0 + @x += 1 + end + end + assert_equal 1, cached.slow_method_cache[:a] + assert_equal 2, cached.slow_method_cache[:b] + assert_equal 3, cached.slow_method_cache[:c] + + assert_equal 1, cached.slow_method_cache[:a] + assert_equal 2, cached.slow_method_cache[:b] + assert_equal 3, cached.slow_method_cache[:c] + + cached.slow_method_cache.clear + + assert_equal 4, cached.slow_method_cache[:a] + assert_equal 5, cached.slow_method_cache[:b] + assert_equal 6, cached.slow_method_cache[:c] + end + + def test_deep_caches_should_work_too + cached do + def slow_method(a, b, c) + a + b + c + end + end + assert_equal 3, cached.slow_method_cache[1][1][1] + assert_equal 7, cached.slow_method_cache[1][2][4] + assert_equal 7, cached.slow_method_cache[1][2][4] + assert_equal 7, cached.slow_method_cache[4][2][1] + + assert_equal({ + 1 => {1 => {1 => 3}, 2 => {4 => 7}}, + 4 => {2 => {1 => 7}}}, + cached.slow_method_cache + ) + end + +end diff --git a/vendor/rails/activesupport/test/class_inheritable_attributes_test.rb b/vendor/rails/activesupport/test/class_inheritable_attributes_test.rb new file mode 100644 index 00000000..36914e2b --- /dev/null +++ b/vendor/rails/activesupport/test/class_inheritable_attributes_test.rb @@ -0,0 +1,141 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../lib/active_support/core_ext/class/inheritable_attributes' + +class ClassInheritableAttributesTest < Test::Unit::TestCase + def setup + @klass = Class.new + end + + def test_reader_declaration + assert_nothing_raised do + @klass.class_inheritable_reader :a + assert_respond_to @klass, :a + assert_respond_to @klass.new, :a + end + end + + def test_writer_declaration + assert_nothing_raised do + @klass.class_inheritable_writer :a + assert_respond_to @klass, :a= + assert_respond_to @klass.new, :a= + end + end + + def test_accessor_declaration + assert_nothing_raised do + @klass.class_inheritable_accessor :a + assert_respond_to @klass, :a + assert_respond_to @klass.new, :a + assert_respond_to @klass, :a= + assert_respond_to @klass.new, :a= + end + end + + def test_array_declaration + assert_nothing_raised do + @klass.class_inheritable_array :a + assert_respond_to @klass, :a + assert_respond_to @klass.new, :a + assert_respond_to @klass, :a= + assert_respond_to @klass.new, :a= + end + end + + def test_hash_declaration + assert_nothing_raised do + @klass.class_inheritable_hash :a + assert_respond_to @klass, :a + assert_respond_to @klass.new, :a + assert_respond_to @klass, :a= + assert_respond_to @klass.new, :a= + end + end + + def test_reader + @klass.class_inheritable_reader :a + assert_nil @klass.a + assert_nil @klass.new.a + + @klass.send(:write_inheritable_attribute, :a, 'a') + + assert_equal 'a', @klass.a + assert_equal 'a', @klass.new.a + assert_equal @klass.a, @klass.new.a + assert_equal @klass.a.object_id, @klass.new.a.object_id + end + + def test_writer + @klass.class_inheritable_reader :a + @klass.class_inheritable_writer :a + + assert_nil @klass.a + assert_nil @klass.new.a + + @klass.a = 'a' + assert_equal 'a', @klass.a + @klass.new.a = 'A' + assert_equal 'A', @klass.a + end + + def test_array + @klass.class_inheritable_array :a + + assert_nil @klass.a + assert_nil @klass.new.a + + @klass.a = %w(a b c) + assert_equal %w(a b c), @klass.a + assert_equal %w(a b c), @klass.new.a + + @klass.new.a = %w(A B C) + assert_equal %w(a b c A B C), @klass.a + assert_equal %w(a b c A B C), @klass.new.a + end + + def test_hash + @klass.class_inheritable_hash :a + + assert_nil @klass.a + assert_nil @klass.new.a + + @klass.a = { :a => 'a' } + assert_equal({ :a => 'a' }, @klass.a) + assert_equal({ :a => 'a' }, @klass.new.a) + + @klass.new.a = { :b => 'b' } + assert_equal({ :a => 'a', :b => 'b' }, @klass.a) + assert_equal({ :a => 'a', :b => 'b' }, @klass.new.a) + end + + def test_inheritance + @klass.class_inheritable_accessor :a + @klass.a = 'a' + + @sub = eval("class FlogMe < @klass; end; FlogMe") + + @klass.class_inheritable_accessor :b + + assert_respond_to @sub, :a + assert_respond_to @sub, :b + assert_equal @klass.a, @sub.a + assert_equal @klass.b, @sub.b + assert_equal 'a', @sub.a + assert_nil @sub.b + + @klass.b = 'b' + assert_not_equal @klass.b, @sub.b + assert_equal 'b', @klass.b + assert_nil @sub.b + + @sub.a = 'A' + assert_not_equal @klass.a, @sub.a + assert_equal 'a', @klass.a + assert_equal 'A', @sub.a + + @sub.b = 'B' + assert_not_equal @klass.b, @sub.b + assert_equal 'b', @klass.b + assert_equal 'B', @sub.b + end +end diff --git a/vendor/rails/activesupport/test/clean_logger_test.rb b/vendor/rails/activesupport/test/clean_logger_test.rb new file mode 100644 index 00000000..fabbc4af --- /dev/null +++ b/vendor/rails/activesupport/test/clean_logger_test.rb @@ -0,0 +1,82 @@ +require 'test/unit' +require 'stringio' +require File.dirname(__FILE__) + '/../lib/active_support/clean_logger' +require File.dirname(__FILE__) + '/../lib/active_support/core_ext/kernel.rb' unless defined? silence_warnings + +class CleanLoggerTest < Test::Unit::TestCase + def setup + @out = StringIO.new + @logger = Logger.new(@out) + end + + def test_format_message + @logger.error 'error' + assert_equal "error\n", @out.string + end + + def test_silence + # Without yielding self. + @logger.silence do + @logger.debug 'debug' + @logger.info 'info' + @logger.warn 'warn' + @logger.error 'error' + @logger.fatal 'fatal' + end + + # Yielding self. + @logger.silence do |logger| + logger.debug 'debug' + logger.info 'info' + logger.warn 'warn' + logger.error 'error' + logger.fatal 'fatal' + end + + # Silencer off. + Logger.silencer = false + @logger.silence do |logger| + logger.warn 'unsilenced' + end + Logger.silencer = true + + assert_equal "error\nfatal\nerror\nfatal\nunsilenced\n", @out.string + end +end + +class CleanLogger_182_to_183_Test < Test::Unit::TestCase + def setup + silence_warnings do + if Logger.method_defined?(:formatter=) + Logger.send(:alias_method, :hide_formatter=, :formatter=) + Logger.send(:undef_method, :formatter=) + else + Logger.send(:define_method, :formatter=) { } + end + load File.dirname(__FILE__) + '/../lib/active_support/clean_logger.rb' + end + + @out = StringIO.new + @logger = Logger.new(@out) + @logger.progname = 'CLEAN LOGGER TEST' + end + + def teardown + silence_warnings do + if Logger.method_defined?(:hide_formatter=) + Logger.send(:alias_method, :formatter=, :hide_formatter=) + else + Logger.send(:undef_method, :formatter=) + end + load File.dirname(__FILE__) + '/../lib/active_support/clean_logger.rb' + end + end + + # Since we've fooled Logger into thinking we're on 1.8.2 if we're on 1.8.3 + # and on 1.8.3 if we're on 1.8.2, it'll define format_message with the + # wrong order of arguments and therefore print progname instead of msg. + def test_format_message_with_faked_version + @logger.error 'error' + assert_equal "CLEAN LOGGER TEST\n", @out.string + end +end diff --git a/vendor/rails/activesupport/test/core_ext/array_ext_test.rb b/vendor/rails/activesupport/test/core_ext/array_ext_test.rb new file mode 100644 index 00000000..0df27fde --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/array_ext_test.rb @@ -0,0 +1,104 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support' + +class ArrayExtToParamTests < Test::Unit::TestCase + def test_string_array + assert_equal '', %w().to_param + assert_equal 'hello/world', %w(hello world).to_param + assert_equal 'hello/10', %w(hello 10).to_param + end + + def test_number_array + assert_equal '10/20', [10, 20].to_param + end +end + +class ArrayExtConversionTests < Test::Unit::TestCase + def test_plain_array_to_sentence + assert_equal "", [].to_sentence + assert_equal "one", ['one'].to_sentence + assert_equal "one and two", ['one', 'two'].to_sentence + assert_equal "one, two, and three", ['one', 'two', 'three'].to_sentence + + end + + def test_to_sentence_with_connector + assert_equal "one, two, and also three", ['one', 'two', 'three'].to_sentence(:connector => 'and also') + end + + def test_to_sentence_with_skip_last_comma + assert_equal "one, two, and three", ['one', 'two', 'three'].to_sentence(:skip_last_comma => false) + end + + def test_two_elements + assert_equal "one and two", ['one', 'two'].to_sentence + end + + def test_one_element + assert_equal "one", ['one'].to_sentence + end +end + +class ArrayExtGroupingTests < Test::Unit::TestCase + def test_group_by_with_perfect_fit + groups = [] + ('a'..'i').to_a.in_groups_of(3) do |group| + groups << group + end + + assert_equal [%w(a b c), %w(d e f), %w(g h i)], groups + end + + def test_group_by_with_padding + groups = [] + ('a'..'g').to_a.in_groups_of(3) do |group| + groups << group + end + + assert_equal [%w(a b c), %w(d e f), ['g', nil, nil]], groups + end + + def test_group_by_pads_with_specified_values + groups = [] + + ('a'..'g').to_a.in_groups_of(3, false) do |group| + groups << group + end + + assert_equal [%w(a b c), %w(d e f), ['g', false, false]], groups + end +end + +class ArraToXmlTests < Test::Unit::TestCase + def test_to_xml + xml = [ + { :name => "David", :age => 26 }, { :name => "Jason", :age => 31 } + ].to_xml(:skip_instruct => true, :indent => 0) + + assert_equal "", xml.first(17) + assert xml.include?(%(26)) + assert xml.include?(%(David)) + assert xml.include?(%(31)) + assert xml.include?(%(Jason)) + end + + def test_to_xml_with_dedicated_name + xml = [ + { :name => "David", :age => 26 }, { :name => "Jason", :age => 31 } + ].to_xml(:skip_instruct => true, :indent => 0, :root => "people") + + assert_equal "", xml.first(16) + end + + def test_to_xml_with_options + xml = [ + { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" } + ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0) + + assert_equal "", xml.first(17) + assert xml.include?(%(Paulina)) + assert xml.include?(%(David)) + assert xml.include?(%(Evergreen)) + assert xml.include?(%(Jason)) + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/test/core_ext/blank_test.rb b/vendor/rails/activesupport/test/core_ext/blank_test.rb new file mode 100644 index 00000000..ff7345e7 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/blank_test.rb @@ -0,0 +1,13 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/object' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/blank' + +class BlankTest < Test::Unit::TestCase + BLANK = [nil, false, '', ' ', " \n\t \r ", [], {}] + NOT = [true, 0, 1, 'a', [nil], { nil => 0 }] + + def test_blank + BLANK.each { |v| assert v.blank? } + NOT.each { |v| assert !v.blank? } + end +end diff --git a/vendor/rails/activesupport/test/core_ext/cgi_ext_test.rb b/vendor/rails/activesupport/test/core_ext/cgi_ext_test.rb new file mode 100644 index 00000000..ec9bb413 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/cgi_ext_test.rb @@ -0,0 +1,15 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/cgi' + +class EscapeSkippingSlashesTest < Test::Unit::TestCase + def test_array + assert_equal 'hello/world', CGI.escape_skipping_slashes(%w(hello world)) + assert_equal 'hello+world/how/are/you', CGI.escape_skipping_slashes(['hello world', 'how', 'are', 'you']) + end + + def test_typical + assert_equal 'hi', CGI.escape_skipping_slashes('hi') + assert_equal 'hi/world', CGI.escape_skipping_slashes('hi/world') + assert_equal 'hi/world+you+funky+thing', CGI.escape_skipping_slashes('hi/world you funky thing') + end +end diff --git a/vendor/rails/activesupport/test/core_ext/class_test.rb b/vendor/rails/activesupport/test/core_ext/class_test.rb new file mode 100644 index 00000000..6e956fa4 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/class_test.rb @@ -0,0 +1,37 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/class' + +class A +end + +module X + class B + end +end + +module Y + module Z + class C + end + end +end + +class ClassTest < Test::Unit::TestCase + def test_removing_class_in_root_namespace + assert A.is_a?(Class) + Class.remove_class(A) + assert_raises(NameError) { A.is_a?(Class) } + end + + def test_removing_class_in_one_level_namespace + assert X::B.is_a?(Class) + Class.remove_class(X::B) + assert_raises(NameError) { X::B.is_a?(Class) } + end + + def test_removing_class_in_two_level_namespace + assert Y::Z::C.is_a?(Class) + Class.remove_class(Y::Z::C) + assert_raises(NameError) { Y::Z::C.is_a?(Class) } + end +end diff --git a/vendor/rails/activesupport/test/core_ext/date_ext_test.rb b/vendor/rails/activesupport/test/core_ext/date_ext_test.rb new file mode 100644 index 00000000..63a0cf5e --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/date_ext_test.rb @@ -0,0 +1,17 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/date' + +class DateExtCalculationsTest < Test::Unit::TestCase + def test_to_s + assert_equal "21 Feb", Date.new(2005, 2, 21).to_s(:short) + assert_equal "February 21, 2005", Date.new(2005, 2, 21).to_s(:long) + end + + def test_to_time + assert_equal Time.local(2005, 2, 21), Date.new(2005, 2, 21).to_time + end + + def test_to_date + assert_equal Date.new(2005, 2, 21), Date.new(2005, 2, 21).to_date + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/test/core_ext/enumerable_test.rb b/vendor/rails/activesupport/test/core_ext/enumerable_test.rb new file mode 100644 index 00000000..6ca41f91 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/enumerable_test.rb @@ -0,0 +1,30 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/enumerable' + +class EnumerableTests < Test::Unit::TestCase + + def test_first_match_no_match + [[1, 2, 3, 4, 5], (1..9)].each {|a| a.first_match {|x| x > 10}} + end + + def test_first_match_with_match + assert_equal true, [1, 2, 3, 4, 5, 6].first_match {|x| x > 4} + assert_equal true, (1..10).first_match {|x| x > 9} + assert_equal :aba, {:a => 10, :aba => 50, :bac => 40}.first_match {|k, v| k if v > 45} + end + + def test_group_by + names = %w(marcel sam david jeremy) + klass = Class.new + klass.send(:attr_accessor, :name) + objects = (1..50).inject([]) do |people,| + p = klass.new + p.name = names.sort_by { rand }.first + people << p + end + + objects.group_by {|object| object.name}.each do |name, group| + assert group.all? {|person| person.name == name} + end + end +end diff --git a/vendor/rails/activesupport/test/core_ext/exception_test.rb b/vendor/rails/activesupport/test/core_ext/exception_test.rb new file mode 100644 index 00000000..9b7afb19 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/exception_test.rb @@ -0,0 +1,65 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/exception' + +class ExceptionExtTests < Test::Unit::TestCase + + def get_exception(cls = RuntimeError, msg = nil, trace = nil) + begin raise cls, msg, (trace || caller) + rescue Object => e + return e + end + end + + def setup + Exception::TraceSubstitutions.clear + end + + def test_clean_backtrace + Exception::TraceSubstitutions << [/\s*hidden.*/, ''] + e = get_exception RuntimeError, 'RAWR', ['bhal.rb', 'rawh hid den stuff is not here', 'almost all'] + assert_kind_of Exception, e + assert_equal ['bhal.rb', 'rawh hid den stuff is not here', 'almost all'], e.clean_backtrace + end + + def test_app_backtrace + Exception::TraceSubstitutions << [/\s*hidden.*/, ''] + e = get_exception RuntimeError, 'RAWR', ['bhal.rb', ' vendor/file.rb some stuff', 'almost all'] + assert_kind_of Exception, e + assert_equal ['bhal.rb', 'almost all'], e.application_backtrace + end + + def test_app_backtrace_with_before + Exception::TraceSubstitutions << [/\s*hidden.*/, ''] + e = get_exception RuntimeError, 'RAWR', ['vendor/file.rb some stuff', 'bhal.rb', ' vendor/file.rb some stuff', 'almost all'] + assert_kind_of Exception, e + assert_equal ['vendor/file.rb some stuff', 'bhal.rb', 'almost all'], e.application_backtrace + end + + def test_framework_backtrace_with_before + Exception::TraceSubstitutions << [/\s*hidden.*/, ''] + e = get_exception RuntimeError, 'RAWR', ['vendor/file.rb some stuff', 'bhal.rb', ' vendor/file.rb some stuff', 'almost all'] + assert_kind_of Exception, e + assert_equal ['vendor/file.rb some stuff', ' vendor/file.rb some stuff'], e.framework_backtrace + end + + def test_backtrace_should_clean_paths + Exception::TraceSubstitutions << [/\s*hidden.*/, ''] + e = get_exception RuntimeError, 'RAWR', ['a/b/c/../d/../../../bhal.rb', 'rawh hid den stuff is not here', 'almost all'] + assert_kind_of Exception, e + assert_equal ['bhal.rb', 'rawh hid den stuff is not here', 'almost all'], e.clean_backtrace + end + + def test_clean_message_should_clean_paths + Exception::TraceSubstitutions << [/\s*hidden.*/, ''] + e = get_exception RuntimeError, "I dislike a/z/x/../../b/y/../c", ['a/b/c/../d/../../../bhal.rb', 'rawh hid den stuff is not here', 'almost all'] + assert_kind_of Exception, e + assert_equal "I dislike a/b/c", e.clean_message + end + + def test_app_trace_should_be_empty_when_no_app_frames + Exception::TraceSubstitutions << [/\s*hidden.*/, ''] + e = get_exception RuntimeError, 'RAWR', ['vendor/file.rb some stuff', 'generated/bhal.rb', ' vendor/file.rb some stuff', 'generated/almost all'] + assert_kind_of Exception, e + assert_equal [], e.application_backtrace + end +end diff --git a/vendor/rails/activesupport/test/core_ext/hash_ext_test.rb b/vendor/rails/activesupport/test/core_ext/hash_ext_test.rb new file mode 100644 index 00000000..3fc3b862 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/hash_ext_test.rb @@ -0,0 +1,241 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support' + +class HashExtTest < Test::Unit::TestCase + def setup + + @strings = { 'a' => 1, 'b' => 2 } + @symbols = { :a => 1, :b => 2 } + @mixed = { :a => 1, 'b' => 2 } + end + + def test_methods + h = {} + assert_respond_to h, :symbolize_keys + assert_respond_to h, :symbolize_keys! + assert_respond_to h, :stringify_keys + assert_respond_to h, :stringify_keys! + assert_respond_to h, :to_options + assert_respond_to h, :to_options! + end + + def test_symbolize_keys + assert_equal @symbols, @symbols.symbolize_keys + assert_equal @symbols, @strings.symbolize_keys + assert_equal @symbols, @mixed.symbolize_keys + + assert_raises(NoMethodError) { { [] => 1 }.symbolize_keys } + end + + def test_symbolize_keys! + assert_equal @symbols, @symbols.dup.symbolize_keys! + assert_equal @symbols, @strings.dup.symbolize_keys! + assert_equal @symbols, @mixed.dup.symbolize_keys! + + assert_raises(NoMethodError) { { [] => 1 }.symbolize_keys } + end + + def test_stringify_keys + assert_equal @strings, @symbols.stringify_keys + assert_equal @strings, @strings.stringify_keys + assert_equal @strings, @mixed.stringify_keys + end + + def test_stringify_keys! + assert_equal @strings, @symbols.dup.stringify_keys! + assert_equal @strings, @strings.dup.stringify_keys! + assert_equal @strings, @mixed.dup.stringify_keys! + end + + def test_indifferent_assorted + @strings = @strings.with_indifferent_access + @symbols = @symbols.with_indifferent_access + @mixed = @mixed.with_indifferent_access + + assert_equal 'a', @strings.send(:convert_key, :a) + + assert_equal 1, @strings.fetch('a') + assert_equal 1, @strings.fetch(:a.to_s) + assert_equal 1, @strings.fetch(:a) + + hashes = { :@strings => @strings, :@symbols => @symbols, :@mixed => @mixed } + method_map = { :'[]' => 1, :fetch => 1, :values_at => [1], + :has_key? => true, :include? => true, :key? => true, + :member? => true } + + hashes.each do |name, hash| + method_map.sort_by { |m| m.to_s }.each do |meth, expected| + assert_equal(expected, hash.send(meth, 'a'), + "Calling #{name}.#{meth} 'a'") + assert_equal(expected, hash.send(meth, :a), + "Calling #{name}.#{meth} :a") + end + end + + assert_equal [1, 2], @strings.values_at('a', 'b') + assert_equal [1, 2], @strings.values_at(:a, :b) + assert_equal [1, 2], @symbols.values_at('a', 'b') + assert_equal [1, 2], @symbols.values_at(:a, :b) + assert_equal [1, 2], @mixed.values_at('a', 'b') + assert_equal [1, 2], @mixed.values_at(:a, :b) + end + + def test_indifferent_writing + hash = HashWithIndifferentAccess.new + hash[:a] = 1 + hash['b'] = 2 + hash[3] = 3 + + assert_equal hash['a'], 1 + assert_equal hash['b'], 2 + assert_equal hash[:a], 1 + assert_equal hash[:b], 2 + assert_equal hash[3], 3 + end + + def test_indifferent_update + hash = HashWithIndifferentAccess.new + hash[:a] = 'a' + hash['b'] = 'b' + + updated_with_strings = hash.update(@strings) + updated_with_symbols = hash.update(@symbols) + updated_with_mixed = hash.update(@mixed) + + assert_equal updated_with_strings[:a], 1 + assert_equal updated_with_strings['a'], 1 + assert_equal updated_with_strings['b'], 2 + + assert_equal updated_with_symbols[:a], 1 + assert_equal updated_with_symbols['b'], 2 + assert_equal updated_with_symbols[:b], 2 + + assert_equal updated_with_mixed[:a], 1 + assert_equal updated_with_mixed['b'], 2 + + assert [updated_with_strings, updated_with_symbols, updated_with_mixed].all? {|hash| hash.keys.size == 2} + end + + def test_indifferent_merging + hash = HashWithIndifferentAccess.new + hash[:a] = 'failure' + hash['b'] = 'failure' + + other = { 'a' => 1, :b => 2 } + + merged = hash.merge(other) + + assert_equal HashWithIndifferentAccess, merged.class + assert_equal 1, merged[:a] + assert_equal 2, merged['b'] + + hash.update(other) + + assert_equal 1, hash[:a] + assert_equal 2, hash['b'] + end + + def test_indifferent_deleting + get_hash = proc{ { :a => 'foo' }.with_indifferent_access } + hash = get_hash.call + assert_equal hash.delete(:a), 'foo' + assert_equal hash.delete(:a), nil + hash = get_hash.call + assert_equal hash.delete('a'), 'foo' + assert_equal hash.delete('a'), nil + end + + def test_assert_valid_keys + assert_nothing_raised do + { :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ]) + { :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny) + end + + assert_raises(ArgumentError, "Unknown key(s): failore") do + { :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ]) + { :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny) + end + end + + def test_indifferent_subhashes + h = {'user' => {'id' => 5}}.with_indifferent_access + ['user', :user].each {|user| [:id, 'id'].each {|id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5"}} + + h = {:user => {:id => 5}}.with_indifferent_access + ['user', :user].each {|user| [:id, 'id'].each {|id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5"}} + end + + def test_assorted_keys_not_stringified + original = {Object.new => 2, 1 => 2, [] => true} + indiff = original.with_indifferent_access + assert(!indiff.keys.any? {|k| k.kind_of? String}, "A key was converted to a string!") + end + + def test_reverse_merge + assert_equal({ :a => 1, :b => 2, :c => 10 }, { :a => 1, :b => 2 }.reverse_merge({:a => "x", :b => "y", :c => 10}) ) + end + + def test_diff + assert_equal({ :a => 2 }, { :a => 2, :b => 5 }.diff({ :a => 1, :b => 5 })) + end +end + +class HashToXmlTest < Test::Unit::TestCase + def setup + @xml_options = { :root => :person, :skip_instruct => true, :indent => 0 } + end + + def test_one_level + xml = { :name => "David", :street => "Paulina" }.to_xml(@xml_options) + assert_equal "", xml.first(8) + assert xml.include?(%(Paulina)) + assert xml.include?(%(David)) + end + + def test_one_level_with_types + xml = { :name => "David", :street => "Paulina", :age => 26, :moved_on => Date.new(2005, 11, 15) }.to_xml(@xml_options) + assert_equal "", xml.first(8) + assert xml.include?(%(Paulina)) + assert xml.include?(%(David)) + assert xml.include?(%(26)) + assert xml.include?(%(2005-11-15)) + end + + def test_one_level_with_nils + xml = { :name => "David", :street => "Paulina", :age => nil }.to_xml(@xml_options) + assert_equal "", xml.first(8) + assert xml.include?(%(Paulina)) + assert xml.include?(%(David)) + assert xml.include?(%()) + end + + def test_one_level_with_skipping_types + xml = { :name => "David", :street => "Paulina", :age => nil }.to_xml(@xml_options.merge(:skip_types => true)) + assert_equal "", xml.first(8) + assert xml.include?(%(Paulina)) + assert xml.include?(%(David)) + assert xml.include?(%()) + end + + def test_two_levels + xml = { :name => "David", :address => { :street => "Paulina" } }.to_xml(@xml_options) + assert_equal "", xml.first(8) + assert xml.include?(%(
      Paulina
      )) + assert xml.include?(%(David)) + end + + def test_two_levels_with_array + xml = { :name => "David", :addresses => [{ :street => "Paulina" }, { :street => "Evergreen" }] }.to_xml(@xml_options) + assert_equal "", xml.first(8) + assert xml.include?(%(
      )) + assert xml.include?(%(
      Paulina
      )) + assert xml.include?(%(
      Evergreen
      )) + assert xml.include?(%(David)) + end + + + def test_three_levels_with_array + xml = { :name => "David", :addresses => [{ :streets => [ { :name => "Paulina" }, { :name => "Paulina" } ] } ] }.to_xml(@xml_options) + assert xml.include?(%(
      )) + end +end diff --git a/vendor/rails/activesupport/test/core_ext/integer_ext_test.rb b/vendor/rails/activesupport/test/core_ext/integer_ext_test.rb new file mode 100644 index 00000000..4435bc79 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/integer_ext_test.rb @@ -0,0 +1,38 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/integer' + +class IntegerExtTest < Test::Unit::TestCase + def test_even + assert [ -2, 0, 2, 4 ].all? { |i| i.even? } + assert ![ -1, 1, 3 ].all? { |i| i.even? } + + assert 22953686867719691230002707821868552601124472329079.odd? + assert !22953686867719691230002707821868552601124472329079.even? + assert 22953686867719691230002707821868552601124472329080.even? + assert !22953686867719691230002707821868552601124472329080.odd? + end + + def test_odd + assert ![ -2, 0, 2, 4 ].all? { |i| i.odd? } + assert [ -1, 1, 3 ].all? { |i| i.odd? } + assert 1000000000000000000000000000000000000000000000000000000001.odd? + end + + def test_multiple_of + [ -7, 0, 7, 14 ].each { |i| assert i.multiple_of?(7) } + [ -7, 7, 14 ].each { |i| assert ! i.multiple_of?(6) } + # test with a prime + assert !22953686867719691230002707821868552601124472329079.multiple_of?(2) + assert !22953686867719691230002707821868552601124472329079.multiple_of?(3) + assert !22953686867719691230002707821868552601124472329079.multiple_of?(5) + assert !22953686867719691230002707821868552601124472329079.multiple_of?(7) + end + + def test_ordinalize + # These tests are mostly just to ensure that the ordinalize method exists + # It's results are tested comprehensively in the inflector test cases. + assert_equal '1st', 1.ordinalize + assert_equal '8th', 8.ordinalize + 1000000000000000000000000000000000000000000000000000000000000000000000.ordinalize + end +end diff --git a/vendor/rails/activesupport/test/core_ext/kernel_test.rb b/vendor/rails/activesupport/test/core_ext/kernel_test.rb new file mode 100644 index 00000000..44e3b872 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/kernel_test.rb @@ -0,0 +1,44 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/kernel' unless defined? silence_warnings + +class KernelTest < Test::Unit::TestCase + def test_silence_warnings + silence_warnings { assert_nil $VERBOSE } + assert_equal 1234, silence_warnings { 1234 } + end + + def test_silence_warnings_verbose_invariant + old_verbose = $VERBOSE + silence_warnings { raise } + flunk + rescue + assert_equal old_verbose, $VERBOSE + end + + + def test_enable_warnings + enable_warnings { assert_equal true, $VERBOSE } + assert_equal 1234, enable_warnings { 1234 } + end + + def test_enable_warnings_verbose_invariant + old_verbose = $VERBOSE + enable_warnings { raise } + flunk + rescue + assert_equal old_verbose, $VERBOSE + end + + + def test_silence_stderr + old_stderr_position = STDERR.tell + silence_stderr { STDERR.puts 'hello world' } + assert_equal old_stderr_position, STDERR.tell + rescue Errno::ESPIPE + # Skip if we can't STDERR.tell + end + + def test_silence_stderr_with_return_value + assert_equal 1, silence_stderr { 1 } + end +end diff --git a/vendor/rails/activesupport/test/core_ext/load_error_tests.rb b/vendor/rails/activesupport/test/core_ext/load_error_tests.rb new file mode 100644 index 00000000..0b24c471 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/load_error_tests.rb @@ -0,0 +1,17 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/load_error' + +class TestMissingSourceFile < Test::Unit::TestCase + def test_with_require + assert_raises(MissingSourceFile) { require 'no_this_file_don\'t_exist' } + end + def test_with_load + assert_raises(MissingSourceFile) { load 'nor_does_this_one' } + end + def test_path + begin load 'nor/this/one.rb' + rescue MissingSourceFile => e + assert_equal 'nor/this/one.rb', e.path + end + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/test/core_ext/module_test.rb b/vendor/rails/activesupport/test/core_ext/module_test.rb new file mode 100644 index 00000000..eb92fbc1 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/module_test.rb @@ -0,0 +1,101 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/class' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/module' + +module One +end + +class Ab + include One +end + +module Xy + class Bc + include One + end +end + +module Yz + module Zy + class Cd + include One + end + end +end + +class De +end + +Somewhere = Struct.new(:street, :city) + +Someone = Struct.new(:name, :place) do + delegate :street, :city, :to => :place + delegate :state, :to => :@place + delegate :upcase, :to => "place.city" +end + +class Name + delegate :upcase, :to => :@full_name + + def initialize(first, last) + @full_name = "#{first} #{last}" + end +end + +$nowhere = <<-EOF +class Name + delegate :nowhere +end +EOF + +$noplace = <<-EOF +class Name + delegate :noplace, :tos => :hollywood +end +EOF + +class ModuleTest < Test::Unit::TestCase + def test_included_in_classes + assert One.included_in_classes.include?(Ab) + assert One.included_in_classes.include?(Xy::Bc) + assert One.included_in_classes.include?(Yz::Zy::Cd) + assert !One.included_in_classes.include?(De) + end + + def test_delegation_to_methods + david = Someone.new("David", Somewhere.new("Paulina", "Chicago")) + assert_equal "Paulina", david.street + assert_equal "Chicago", david.city + end + + def test_delegation_down_hierarchy + david = Someone.new("David", Somewhere.new("Paulina", "Chicago")) + assert_equal "CHICAGO", david.upcase + end + + def test_delegation_to_instance_variable + david = Name.new("David", "Hansson") + assert_equal "DAVID HANSSON", david.upcase + end + + def test_missing_delegation_target + assert_raises(ArgumentError) { eval($nowhere) } + assert_raises(ArgumentError) { eval($noplace) } + end + + def test_parent + assert_equal Yz::Zy, Yz::Zy::Cd.parent + assert_equal Yz, Yz::Zy.parent + assert_equal Object, Yz.parent + end + + def test_parents + assert_equal [Yz::Zy, Yz, Object], Yz::Zy::Cd.parents + assert_equal [Yz, Object], Yz::Zy.parents + end + + def test_as_load_path + assert_equal 'yz/zy', Yz::Zy.as_load_path + assert_equal 'yz', Yz.as_load_path + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/test/core_ext/numeric_ext_test.rb b/vendor/rails/activesupport/test/core_ext/numeric_ext_test.rb new file mode 100644 index 00000000..828276d4 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/numeric_ext_test.rb @@ -0,0 +1,58 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/numeric' + +class NumericExtTimeTest < Test::Unit::TestCase + def setup + @now = Time.now + @seconds = { + 1.minute => 60, + 10.minutes => 600, + 1.hour + 15.minutes => 4500, + 2.days + 4.hours + 30.minutes => 189000, + 5.years + 1.month + 1.fortnight => 161589600 + } + end + + def test_units + @seconds.each do |actual, expected| + assert_equal expected, actual + end + end + + def test_intervals + @seconds.values.each do |seconds| + assert_equal seconds.since(@now), @now + seconds + assert_equal seconds.until(@now), @now - seconds + end + end + + # Test intervals based from Time.now + def test_now + @seconds.values.each do |seconds| + now = Time.now + assert seconds.ago >= now - seconds + now = Time.now + assert seconds.from_now >= now + seconds + end + end +end + +class NumericExtSizeTest < Test::Unit::TestCase + def test_unit_in_terms_of_another + relationships = { + 1024.kilobytes => 1.megabyte, + 3584.0.kilobytes => 3.5.megabytes, + 3584.0.megabytes => 3.5.gigabytes, + 1.kilobyte ** 4 => 1.terabyte, + 1024.kilobytes + 2.megabytes => 3.megabytes, + 2.gigabytes / 4 => 512.megabytes, + 256.megabytes * 20 + 5.gigabytes => 10.gigabytes, + 1.kilobyte ** 5 => 1.petabyte, + 1.kilobyte ** 6 => 1.exabyte + } + + relationships.each do |left, right| + assert_equal right, left + end + end +end diff --git a/vendor/rails/activesupport/test/core_ext/object_and_class_ext_test.rb b/vendor/rails/activesupport/test/core_ext/object_and_class_ext_test.rb new file mode 100644 index 00000000..a1e5ea79 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/object_and_class_ext_test.rb @@ -0,0 +1,153 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/object' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/class' + +class ClassA; end +class ClassB < ClassA; end +class ClassC < ClassB; end +class ClassD < ClassA; end + +class ClassI; end +class ClassJ < ClassI; end + +class ClassK +end +module Nested + class ClassL < ClassK + end +end + +module Bar + def bar; end +end + +module Baz + def baz; end +end + +class Foo + include Bar +end + +class ClassExtTest < Test::Unit::TestCase + def test_methods + assert defined?(ClassB) + assert defined?(ClassC) + assert defined?(ClassD) + + ClassA.remove_subclasses + + assert !defined?(ClassB) + assert !defined?(ClassC) + assert !defined?(ClassD) + end + + def test_subclasses_of + assert_equal [ClassJ], Object.subclasses_of(ClassI) + ClassI.remove_subclasses + assert_equal [], Object.subclasses_of(ClassI) + end + + def test_subclasses_of_should_find_nested_classes + assert Object.subclasses_of(ClassK).include?(Nested::ClassL) + end + + def test_subclasses_of_should_not_return_removed_classes + # First create the removed class + old_class = Nested.send :remove_const, :ClassL + new_class = Class.new(ClassK) + Nested.const_set :ClassL, new_class + assert_equal "Nested::ClassL", new_class.name # Sanity check + + subclasses = Object.subclasses_of(ClassK) + assert subclasses.include?(new_class) + assert ! subclasses.include?(old_class) + end +end + +class ObjectTests < Test::Unit::TestCase + def test_suppress_re_raises + assert_raises(LoadError) { suppress(ArgumentError) {raise LoadError} } + end + def test_suppress_supresses + suppress(ArgumentError) { raise ArgumentError } + suppress(LoadError) { raise LoadError } + suppress(LoadError, ArgumentError) { raise LoadError } + suppress(LoadError, ArgumentError) { raise ArgumentError } + end + + def test_extended_by + foo = Foo.new + assert foo.extended_by.include?(Bar) + foo.extend(Baz) + assert ([Bar, Baz] - foo.extended_by).empty?, "Expected Bar, Baz in #{foo.extended_by.inspect}" + end + + def test_extend_with_included_modules_from + foo, object = Foo.new, Object.new + assert !object.respond_to?(:bar) + assert !object.respond_to?(:baz) + + object.extend_with_included_modules_from(foo) + assert object.respond_to?(:bar) + assert !object.respond_to?(:baz) + + foo.extend(Baz) + object.extend_with_included_modules_from(foo) + assert object.respond_to?(:bar) + assert object.respond_to?(:baz) + end + +end + +class ObjectInstanceVariableTest < Test::Unit::TestCase + def setup + @source, @dest = Object.new, Object.new + @source.instance_variable_set(:@bar, 'bar') + @source.instance_variable_set(:@baz, 'baz') + end + + def test_copy_instance_variables_from_without_explicit_excludes + assert_equal [], @dest.instance_variables + @dest.copy_instance_variables_from(@source) + + assert_equal %w(@bar @baz), @dest.instance_variables.sort + %w(@bar @baz).each do |name| + assert_equal @source.instance_variable_get(name).object_id, + @dest.instance_variable_get(name).object_id + end + end + + def test_copy_instance_variables_from_with_explicit_excludes + @dest.copy_instance_variables_from(@source, ['@baz']) + assert !@dest.instance_variables.include?('@baz') + assert_equal 'bar', @dest.instance_variable_get('@bar') + end + + def test_copy_instance_variables_automatically_excludes_protected_instance_variables + @source.instance_variable_set(:@quux, 'quux') + class << @source + def protected_instance_variables + ['@bar', :@quux] + end + end + + @dest.copy_instance_variables_from(@source) + assert !@dest.instance_variables.include?('@bar') + assert !@dest.instance_variables.include?('@quux') + assert_equal 'baz', @dest.instance_variable_get('@baz') + end + + def test_instance_values + object = Object.new + object.instance_variable_set :@a, 1 + object.instance_variable_set :@b, 2 + assert_equal({'a' => 1, 'b' => 2}, object.instance_values) + end + + def test_instance_exec_passes_arguments_to_block + block = Proc.new { |value| [self, value] } + assert_equal %w(hello goodbye), 'hello'.instance_exec('goodbye', &block) + end + +end diff --git a/vendor/rails/activesupport/test/core_ext/pathname_test.rb b/vendor/rails/activesupport/test/core_ext/pathname_test.rb new file mode 100644 index 00000000..3f49b665 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/pathname_test.rb @@ -0,0 +1,12 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/pathname' + +class TestPathname < Test::Unit::TestCase + + def test_clean_within + assert_equal "Hi", Pathname.clean_within("Hi") + assert_equal "Hi", Pathname.clean_within("Hi/a/b/../..") + assert_equal "Hello\nWorld", Pathname.clean_within("Hello/a/b/../..\na/b/../../World/c/..") + end + +end diff --git a/vendor/rails/activesupport/test/core_ext/proc_test.rb b/vendor/rails/activesupport/test/core_ext/proc_test.rb new file mode 100644 index 00000000..ca91a257 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/proc_test.rb @@ -0,0 +1,12 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/proc' + +class ProcTests < Test::Unit::TestCase + def test_bind_returns_method_with_changed_self + block = Proc.new { self } + assert_equal self, block.call + bound_block = block.bind("hello") + assert_not_equal block, bound_block + assert_equal "hello", bound_block.call + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/test/core_ext/range_ext_test.rb b/vendor/rails/activesupport/test/core_ext/range_ext_test.rb new file mode 100644 index 00000000..49cbb10b --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/range_ext_test.rb @@ -0,0 +1,16 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/date' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/time' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/range' + +class RangeTest < Test::Unit::TestCase + def test_to_s_from_dates + date_range = Date.new(2005, 12, 10)..Date.new(2005, 12, 12) + assert_equal "BETWEEN '2005-12-10' AND '2005-12-12'", date_range.to_s(:db) + end + + def test_to_s_from_times + date_range = Time.utc(2005, 12, 10, 15, 30)..Time.utc(2005, 12, 10, 17, 30) + assert_equal "BETWEEN '2005-12-10 15:30:00' AND '2005-12-10 17:30:00'", date_range.to_s(:db) + end +end diff --git a/vendor/rails/activesupport/test/core_ext/string_ext_test.rb b/vendor/rails/activesupport/test/core_ext/string_ext_test.rb new file mode 100644 index 00000000..e09eb021 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/string_ext_test.rb @@ -0,0 +1,108 @@ +require 'test/unit' +require 'date' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/string' +require File.dirname(__FILE__) + '/../inflector_test' unless defined? InflectorTest + +class StringInflectionsTest < Test::Unit::TestCase + def test_pluralize + InflectorTest::SingularToPlural.each do |singular, plural| + assert_equal(plural, singular.pluralize) + end + + assert_equal("plurals", "plurals".pluralize) + end + + def test_singularize + InflectorTest::SingularToPlural.each do |singular, plural| + assert_equal(singular, plural.singularize) + end + end + + def test_camelize + InflectorTest::CamelToUnderscore.each do |camel, underscore| + assert_equal(camel, underscore.camelize) + end + end + + def test_underscore + InflectorTest::CamelToUnderscore.each do |camel, underscore| + assert_equal(underscore, camel.underscore) + end + + assert_equal "html_tidy", "HTMLTidy".underscore + assert_equal "html_tidy_generator", "HTMLTidyGenerator".underscore + end + + def test_demodulize + assert_equal "Account", Inflector.demodulize("MyApplication::Billing::Account") + end + + def test_foreign_key + InflectorTest::ClassNameToForeignKeyWithUnderscore.each do |klass, foreign_key| + assert_equal(foreign_key, klass.foreign_key) + end + + InflectorTest::ClassNameToForeignKeyWithoutUnderscore.each do |klass, foreign_key| + assert_equal(foreign_key, klass.foreign_key(false)) + end + end + + def test_tableize + InflectorTest::ClassNameToTableName.each do |class_name, table_name| + assert_equal(table_name, class_name.tableize) + end + end + + def test_classify + InflectorTest::ClassNameToTableName.each do |class_name, table_name| + assert_equal(class_name, table_name.classify) + end + end + + def test_string_to_time + assert_equal Time.utc(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time + assert_equal Time.local(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time(:local) + assert_equal Date.new(2005, 2, 27), "2005-02-27".to_date + end + + def test_access + s = "hello" + assert_equal "h", s.at(0) + + assert_equal "llo", s.from(2) + assert_equal "hel", s.to(2) + + assert_equal "h", s.first + assert_equal "he", s.first(2) + + assert_equal "o", s.last + assert_equal "llo", s.last(3) + + assert_equal 'x', 'x'.first + assert_equal 'x', 'x'.first(4) + + assert_equal 'x', 'x'.last + assert_equal 'x', 'x'.last(4) + end + + def test_starts_ends_with + s = "hello" + assert s.starts_with?('h') + assert s.starts_with?('hel') + assert !s.starts_with?('el') + + assert s.ends_with?('o') + assert s.ends_with?('lo') + assert !s.ends_with?('el') + end + + def test_each_char_with_utf8_string_when_kcode_is_utf8 + old_kcode, $KCODE = $KCODE, 'UTF8' + '€2.99'.each_char do |char| + assert_not_equal 1, char.length + break + end + ensure + $KCODE = old_kcode + end +end diff --git a/vendor/rails/activesupport/test/core_ext/symbol_test.rb b/vendor/rails/activesupport/test/core_ext/symbol_test.rb new file mode 100644 index 00000000..fec504d9 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/symbol_test.rb @@ -0,0 +1,8 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/symbol' + +class SymbolTests < Test::Unit::TestCase + def test_to_proc + assert_equal %w(one two three), [:one, :two, :three].map(&:to_s) + end +end diff --git a/vendor/rails/activesupport/test/core_ext/time_ext_test.rb b/vendor/rails/activesupport/test/core_ext/time_ext_test.rb new file mode 100644 index 00000000..dee0a1f8 --- /dev/null +++ b/vendor/rails/activesupport/test/core_ext/time_ext_test.rb @@ -0,0 +1,209 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/numeric' +require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/time' + +class TimeExtCalculationsTest < Test::Unit::TestCase + def test_seconds_since_midnight + assert_equal 1,Time.local(2005,1,1,0,0,1).seconds_since_midnight + assert_equal 60,Time.local(2005,1,1,0,1,0).seconds_since_midnight + assert_equal 3660,Time.local(2005,1,1,1,1,0).seconds_since_midnight + assert_equal 86399,Time.local(2005,1,1,23,59,59).seconds_since_midnight + assert_equal 60.00001,Time.local(2005,1,1,0,1,0,10).seconds_since_midnight + end + + def test_begining_of_week + assert_equal Time.local(2005,1,31), Time.local(2005,2,4,10,10,10).beginning_of_week + assert_equal Time.local(2005,11,28), Time.local(2005,11,28,0,0,0).beginning_of_week #monday + assert_equal Time.local(2005,11,28), Time.local(2005,11,29,0,0,0).beginning_of_week #tuesday + assert_equal Time.local(2005,11,28), Time.local(2005,11,30,0,0,0).beginning_of_week #wednesday + assert_equal Time.local(2005,11,28), Time.local(2005,12,01,0,0,0).beginning_of_week #thursday + assert_equal Time.local(2005,11,28), Time.local(2005,12,02,0,0,0).beginning_of_week #friday + assert_equal Time.local(2005,11,28), Time.local(2005,12,03,0,0,0).beginning_of_week #saturday + assert_equal Time.local(2005,11,28), Time.local(2005,12,04,0,0,0).beginning_of_week #sunday + end + + def test_beginning_of_day + assert_equal Time.local(2005,2,4,0,0,0), Time.local(2005,2,4,10,10,10).beginning_of_day + end + + def test_beginning_of_month + assert_equal Time.local(2005,2,1,0,0,0), Time.local(2005,2,22,10,10,10).beginning_of_month + end + + def test_beginning_of_quarter + assert_equal Time.local(2005,1,1,0,0,0), Time.local(2005,2,15,10,10,10).beginning_of_quarter + assert_equal Time.local(2005,1,1,0,0,0), Time.local(2005,1,1,0,0,0).beginning_of_quarter + assert_equal Time.local(2005,10,1,0,0,0), Time.local(2005,12,31,10,10,10).beginning_of_quarter + assert_equal Time.local(2005,4,1,0,0,0), Time.local(2005,6,30,23,59,59).beginning_of_quarter + end + + def test_end_of_month + assert_equal Time.local(2005,3,31,0,0,0), Time.local(2005,3,20,10,10,10).end_of_month + assert_equal Time.local(2005,2,28,0,0,0), Time.local(2005,2,20,10,10,10).end_of_month + assert_equal Time.local(2005,4,30,0,0,0), Time.local(2005,4,20,10,10,10).end_of_month + + end + + def test_beginning_of_year + assert_equal Time.local(2005,1,1,0,0,0), Time.local(2005,2,22,10,10,10).beginning_of_year + end + + def test_months_ago + assert_equal Time.local(2005,5,5,10), Time.local(2005,6,5,10,0,0).months_ago(1) + assert_equal Time.local(2004,11,5,10), Time.local(2005,6,5,10,0,0).months_ago(7) + assert_equal Time.local(2004,12,5,10), Time.local(2005,6,5,10,0,0).months_ago(6) + assert_equal Time.local(2004,6,5,10), Time.local(2005,6,5,10,0,0).months_ago(12) + assert_equal Time.local(2003,6,5,10), Time.local(2005,6,5,10,0,0).months_ago(24) + end + + def test_months_since + assert_equal Time.local(2005,7,5,10), Time.local(2005,6,5,10,0,0).months_since(1) + assert_equal Time.local(2006,1,5,10), Time.local(2005,12,5,10,0,0).months_since(1) + assert_equal Time.local(2005,12,5,10), Time.local(2005,6,5,10,0,0).months_since(6) + assert_equal Time.local(2006,6,5,10), Time.local(2005,12,5,10,0,0).months_since(6) + assert_equal Time.local(2006,1,5,10), Time.local(2005,6,5,10,0,0).months_since(7) + assert_equal Time.local(2006,6,5,10), Time.local(2005,6,5,10,0,0).months_since(12) + assert_equal Time.local(2007,6,5,10), Time.local(2005,6,5,10,0,0).months_since(24) + end + + def test_years_ago + assert_equal Time.local(2004,6,5,10), Time.local(2005,6,5,10,0,0).years_ago(1) + assert_equal Time.local(1998,6,5,10), Time.local(2005,6,5,10,0,0).years_ago(7) + end + + def test_years_since + assert_equal Time.local(2006,6,5,10), Time.local(2005,6,5,10,0,0).years_since(1) + assert_equal Time.local(2012,6,5,10), Time.local(2005,6,5,10,0,0).years_since(7) + # Failure because of size limitations of numeric? + # assert_equal Time.local(2182,6,5,10), Time.local(2005,6,5,10,0,0).years_since(177) + end + + def test_last_year + assert_equal Time.local(2004,6,5,10), Time.local(2005,6,5,10,0,0).last_year + end + + + def test_ago + assert_equal Time.local(2005,2,22,10,10,9), Time.local(2005,2,22,10,10,10).ago(1) + assert_equal Time.local(2005,2,22,9,10,10), Time.local(2005,2,22,10,10,10).ago(3600) + assert_equal Time.local(2005,2,20,10,10,10), Time.local(2005,2,22,10,10,10).ago(86400*2) + assert_equal Time.local(2005,2,20,9,9,45), Time.local(2005,2,22,10,10,10).ago(86400*2 + 3600 + 25) + end + + def test_since + assert_equal Time.local(2005,2,22,10,10,11), Time.local(2005,2,22,10,10,10).since(1) + assert_equal Time.local(2005,2,22,11,10,10), Time.local(2005,2,22,10,10,10).since(3600) + assert_equal Time.local(2005,2,24,10,10,10), Time.local(2005,2,22,10,10,10).since(86400*2) + assert_equal Time.local(2005,2,24,11,10,35), Time.local(2005,2,22,10,10,10).since(86400*2 + 3600 + 25) + end + + def test_yesterday + assert_equal Time.local(2005,2,21,10,10,10), Time.local(2005,2,22,10,10,10).yesterday + assert_equal Time.local(2005,2,28,10,10,10), Time.local(2005,3,2,10,10,10).yesterday.yesterday + end + + def test_tomorrow + assert_equal Time.local(2005,2,23,10,10,10), Time.local(2005,2,22,10,10,10).tomorrow + assert_equal Time.local(2005,3,2,10,10,10), Time.local(2005,2,28,10,10,10).tomorrow.tomorrow + end + + def test_change + assert_equal Time.local(2006,2,22,15,15,10), Time.local(2005,2,22,15,15,10).change(:year => 2006) + assert_equal Time.local(2005,6,22,15,15,10), Time.local(2005,2,22,15,15,10).change(:month => 6) + assert_equal Time.local(2012,9,22,15,15,10), Time.local(2005,2,22,15,15,10).change(:year => 2012, :month => 9) + assert_equal Time.local(2005,2,22,16), Time.local(2005,2,22,15,15,10).change(:hour => 16) + assert_equal Time.local(2005,2,22,16,45), Time.local(2005,2,22,15,15,10).change(:hour => 16, :min => 45) + assert_equal Time.local(2005,2,22,15,45), Time.local(2005,2,22,15,15,10).change(:min => 45) + + assert_equal Time.local(2005,1,2, 5, 0, 0, 0), Time.local(2005,1,2,11,22,33,44).change(:hour => 5) + assert_equal Time.local(2005,1,2,11, 6, 0, 0), Time.local(2005,1,2,11,22,33,44).change(:min => 6) + assert_equal Time.local(2005,1,2,11,22, 7, 0), Time.local(2005,1,2,11,22,33,44).change(:sec => 7) + assert_equal Time.local(2005,1,2,11,22,33, 8), Time.local(2005,1,2,11,22,33,44).change(:usec => 8) + end + + def test_utc_change + assert_equal Time.utc(2006,2,22,15,15,10), Time.utc(2005,2,22,15,15,10).change(:year => 2006) + assert_equal Time.utc(2005,6,22,15,15,10), Time.utc(2005,2,22,15,15,10).change(:month => 6) + assert_equal Time.utc(2012,9,22,15,15,10), Time.utc(2005,2,22,15,15,10).change(:year => 2012, :month => 9) + assert_equal Time.utc(2005,2,22,16), Time.utc(2005,2,22,15,15,10).change(:hour => 16) + assert_equal Time.utc(2005,2,22,16,45), Time.utc(2005,2,22,15,15,10).change(:hour => 16, :min => 45) + assert_equal Time.utc(2005,2,22,15,45), Time.utc(2005,2,22,15,15,10).change(:min => 45) + end + + def test_plus + assert_equal Time.local(2006,2,28,15,15,10), Time.local(2005,2,28,15,15,10).advance(:years => 1) + assert_equal Time.local(2005,6,28,15,15,10), Time.local(2005,2,28,15,15,10).advance(:months => 4) + assert_equal Time.local(2012,9,28,15,15,10), Time.local(2005,2,28,15,15,10).advance(:years => 7, :months => 7) + assert_equal Time.local(2013,10,3,15,15,10), Time.local(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :days => 5) + end + + def test_utc_plus + assert_equal Time.utc(2006,2,22,15,15,10), Time.utc(2005,2,22,15,15,10).advance(:years => 1) + assert_equal Time.utc(2005,6,22,15,15,10), Time.utc(2005,2,22,15,15,10).advance(:months => 4) + assert_equal Time.utc(2012,9,22,15,15,10), Time.utc(2005,2,22,15,15,10).advance(:years => 7, :months => 7) + assert_equal Time.utc(2013,10,3,15,15,10), Time.utc(2005,2,22,15,15,10).advance(:years => 7, :months => 19, :days => 11) + end + + def test_next_week + assert_equal Time.local(2005,2,28), Time.local(2005,2,22,15,15,10).next_week + assert_equal Time.local(2005,2,29), Time.local(2005,2,22,15,15,10).next_week(:tuesday) + assert_equal Time.local(2005,3,4), Time.local(2005,2,22,15,15,10).next_week(:friday) + end + + def test_to_s + time = Time.local(2005, 2, 21, 17, 44, 30) + assert_equal "2005-02-21 17:44:30", time.to_s(:db) + assert_equal "21 Feb 17:44", time.to_s(:short) + assert_equal "February 21, 2005 17:44", time.to_s(:long) + + time = Time.utc(2005, 2, 21, 17, 44, 30) + assert_equal "Mon, 21 Feb 2005 17:44:30 +0000", time.to_s(:rfc822) + end + + def test_to_date + assert_equal Date.new(2005, 2, 21), Time.local(2005, 2, 21, 17, 44, 30).to_date + end + + def test_to_time + assert_equal Time.local(2005, 2, 21, 17, 44, 30), Time.local(2005, 2, 21, 17, 44, 30).to_time + end + + # NOTE: this test seems to fail (changeset 1958) only on certain platforms, + # like OSX, and FreeBSD 5.4. + def test_fp_inaccuracy_ticket_1836 + midnight = Time.local(2005, 2, 21, 0, 0, 0) + assert_equal midnight.midnight, (midnight + 1.hour + 0.000001).midnight + end + + def test_days_in_month + assert_equal 31, Time.days_in_month(1, 2005) + + assert_equal 28, Time.days_in_month(2, 2005) + assert_equal 29, Time.days_in_month(2, 2004) + assert_equal 29, Time.days_in_month(2, 2000) + assert_equal 28, Time.days_in_month(2, 1900) + + assert_equal 31, Time.days_in_month(3, 2005) + assert_equal 30, Time.days_in_month(4, 2005) + assert_equal 31, Time.days_in_month(5, 2005) + assert_equal 30, Time.days_in_month(6, 2005) + assert_equal 31, Time.days_in_month(7, 2005) + assert_equal 31, Time.days_in_month(8, 2005) + assert_equal 30, Time.days_in_month(9, 2005) + assert_equal 31, Time.days_in_month(10, 2005) + assert_equal 30, Time.days_in_month(11, 2005) + assert_equal 31, Time.days_in_month(12, 2005) + end + + def test_next_month_on_31st + assert_equal Time.local(2005, 9, 30), Time.local(2005, 8, 31).next_month + end + + def test_last_month_on_31st + assert_equal Time.local(2004, 2, 29), Time.local(2004, 3, 31).last_month + end + + def test_xmlschema_is_available + assert_nothing_raised { Time.now.xmlschema } + end +end diff --git a/vendor/rails/activesupport/test/dependencies/check_warnings.rb b/vendor/rails/activesupport/test/dependencies/check_warnings.rb new file mode 100644 index 00000000..03c3dca1 --- /dev/null +++ b/vendor/rails/activesupport/test/dependencies/check_warnings.rb @@ -0,0 +1,2 @@ +$check_warnings_load_count += 1 +$checked_verbose = $VERBOSE diff --git a/vendor/rails/activesupport/test/dependencies/mutual_one.rb b/vendor/rails/activesupport/test/dependencies/mutual_one.rb new file mode 100644 index 00000000..576eb317 --- /dev/null +++ b/vendor/rails/activesupport/test/dependencies/mutual_one.rb @@ -0,0 +1,4 @@ +$mutual_dependencies_count += 1 +require_dependency 'mutual_two' +require_dependency 'mutual_two.rb' +require_dependency 'mutual_two' diff --git a/vendor/rails/activesupport/test/dependencies/mutual_two.rb b/vendor/rails/activesupport/test/dependencies/mutual_two.rb new file mode 100644 index 00000000..fdbc2dcd --- /dev/null +++ b/vendor/rails/activesupport/test/dependencies/mutual_two.rb @@ -0,0 +1,4 @@ +$mutual_dependencies_count += 1 +require_dependency 'mutual_one.rb' +require_dependency 'mutual_one' +require_dependency 'mutual_one.rb' diff --git a/vendor/rails/activesupport/test/dependencies/raises_exception.rb b/vendor/rails/activesupport/test/dependencies/raises_exception.rb new file mode 100644 index 00000000..69750eee --- /dev/null +++ b/vendor/rails/activesupport/test/dependencies/raises_exception.rb @@ -0,0 +1,3 @@ +$raises_exception_load_count += 1 +raise 'Loading me failed, so do not add to loaded or history.' +$raises_exception_load_count += 1 diff --git a/vendor/rails/activesupport/test/dependencies/service_one.rb b/vendor/rails/activesupport/test/dependencies/service_one.rb new file mode 100644 index 00000000..f43bfea2 --- /dev/null +++ b/vendor/rails/activesupport/test/dependencies/service_one.rb @@ -0,0 +1,5 @@ +$loaded_service_one ||= 0 +$loaded_service_one += 1 + +class ServiceOne +end \ No newline at end of file diff --git a/vendor/rails/activesupport/test/dependencies/service_two.rb b/vendor/rails/activesupport/test/dependencies/service_two.rb new file mode 100644 index 00000000..5205a78b --- /dev/null +++ b/vendor/rails/activesupport/test/dependencies/service_two.rb @@ -0,0 +1,2 @@ +class ServiceTwo +end \ No newline at end of file diff --git a/vendor/rails/activesupport/test/dependencies_test.rb b/vendor/rails/activesupport/test/dependencies_test.rb new file mode 100644 index 00000000..f5651edd --- /dev/null +++ b/vendor/rails/activesupport/test/dependencies_test.rb @@ -0,0 +1,161 @@ +require 'test/unit' +$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib/active_support/' +require 'core_ext/string' +require 'dependencies' + +class DependenciesTest < Test::Unit::TestCase + def teardown + Dependencies.clear + end + + def with_loading(from_dir = nil) + prior_path = $LOAD_PATH.clone + $LOAD_PATH.unshift "#{File.dirname(__FILE__)}/#{from_dir}" if from_dir + old_mechanism, Dependencies.mechanism = Dependencies.mechanism, :load + yield + ensure + $LOAD_PATH.clear + $LOAD_PATH.concat prior_path + Dependencies.mechanism = old_mechanism + end + + def test_tracking_loaded_files + require_dependency(File.dirname(__FILE__) + "/dependencies/service_one") + require_dependency(File.dirname(__FILE__) + "/dependencies/service_two") + assert_equal 2, Dependencies.loaded.size + end + + def test_tracking_identical_loaded_files + require_dependency(File.dirname(__FILE__) + "/dependencies/service_one") + require_dependency(File.dirname(__FILE__) + "/dependencies/service_one") + assert_equal 1, Dependencies.loaded.size + end + + def test_missing_dependency_raises_missing_source_file + assert_raises(MissingSourceFile) { require_dependency("missing_service") } + end + + def test_missing_association_raises_nothing + assert_nothing_raised { require_association("missing_model") } + end + + def test_dependency_which_raises_exception_isnt_added_to_loaded_set + with_loading do + filename = "#{File.dirname(__FILE__)}/dependencies/raises_exception" + $raises_exception_load_count = 0 + + 5.times do |count| + assert_raises(RuntimeError) { require_dependency filename } + assert_equal count + 1, $raises_exception_load_count + + assert !Dependencies.loaded.include?(filename) + assert !Dependencies.history.include?(filename) + end + end + end + + def test_warnings_should_be_enabled_on_first_load + with_loading do + old_warnings, Dependencies.warnings_on_first_load = Dependencies.warnings_on_first_load, true + + filename = "#{File.dirname(__FILE__)}/dependencies/check_warnings" + $check_warnings_load_count = 0 + + assert !Dependencies.loaded.include?(filename) + assert !Dependencies.history.include?(filename) + + silence_warnings { require_dependency filename } + assert_equal 1, $check_warnings_load_count + assert_equal true, $checked_verbose, 'On first load warnings should be enabled.' + + assert Dependencies.loaded.include?(filename) + Dependencies.clear + assert !Dependencies.loaded.include?(filename) + assert Dependencies.history.include?(filename) + + silence_warnings { require_dependency filename } + assert_equal 2, $check_warnings_load_count + assert_equal nil, $checked_verbose, 'After first load warnings should be left alone.' + + assert Dependencies.loaded.include?(filename) + Dependencies.clear + assert !Dependencies.loaded.include?(filename) + assert Dependencies.history.include?(filename) + + enable_warnings { require_dependency filename } + assert_equal 3, $check_warnings_load_count + assert_equal true, $checked_verbose, 'After first load warnings should be left alone.' + + assert Dependencies.loaded.include?(filename) + end + end + + def test_mutual_dependencies_dont_infinite_loop + with_loading 'dependencies' do + $mutual_dependencies_count = 0 + assert_nothing_raised { require_dependency 'mutual_one' } + assert_equal 2, $mutual_dependencies_count + + Dependencies.clear + + $mutual_dependencies_count = 0 + assert_nothing_raised { require_dependency 'mutual_two' } + assert_equal 2, $mutual_dependencies_count + end + end + + def test_as_load_path + assert_equal '', DependenciesTest.as_load_path + end + + def test_module_loading + with_loading 'autoloading_fixtures' do + assert_kind_of Module, A + assert_kind_of Class, A::B + assert_kind_of Class, A::C::D + assert_kind_of Class, A::C::E::F + end + end + + def test_non_existing_const_raises_name_error + with_loading 'autoloading_fixtures' do + assert_raises(NameError) { DoesNotExist } + assert_raises(NameError) { NoModule::DoesNotExist } + assert_raises(NameError) { A::DoesNotExist } + assert_raises(NameError) { A::B::DoesNotExist } + end + end + + def test_directories_should_manifest_as_modules + with_loading 'autoloading_fixtures' do + assert_kind_of Module, ModuleFolder + Object.send :remove_const, :ModuleFolder + end + end + + def test_nested_class_access + with_loading 'autoloading_fixtures' do + assert_kind_of Class, ModuleFolder::NestedClass + Object.send :remove_const, :ModuleFolder + end + end + + def test_nested_class_can_access_sibling + with_loading 'autoloading_fixtures' do + sibling = ModuleFolder::NestedClass.class_eval "NestedSibling" + assert defined?(ModuleFolder::NestedSibling) + assert_equal ModuleFolder::NestedSibling, sibling + Object.send :remove_const, :ModuleFolder + end + end + + def failing_test_access_thru_and_upwards_fails + with_loading 'autoloading_fixtures' do + assert ! defined?(ModuleFolder) + assert_raises(NameError) { ModuleFolder::Object } + assert_raises(NameError) { ModuleFolder::NestedClass::Object } + Object.send :remove_const, :ModuleFolder + end + end + +end diff --git a/vendor/rails/activesupport/test/inflector_test.rb b/vendor/rails/activesupport/test/inflector_test.rb new file mode 100644 index 00000000..c0a2b76a --- /dev/null +++ b/vendor/rails/activesupport/test/inflector_test.rb @@ -0,0 +1,324 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../lib/active_support/inflector' unless defined? Inflector + +module Ace + module Base + class Case + end + end +end + +class InflectorTest < Test::Unit::TestCase + SingularToPlural = { + "search" => "searches", + "switch" => "switches", + "fix" => "fixes", + "box" => "boxes", + "process" => "processes", + "address" => "addresses", + "case" => "cases", + "stack" => "stacks", + "wish" => "wishes", + "fish" => "fish", + + "category" => "categories", + "query" => "queries", + "ability" => "abilities", + "agency" => "agencies", + "movie" => "movies", + + "archive" => "archives", + + "index" => "indices", + + "wife" => "wives", + "safe" => "saves", + "half" => "halves", + + "move" => "moves", + + "salesperson" => "salespeople", + "person" => "people", + + "spokesman" => "spokesmen", + "man" => "men", + "woman" => "women", + + "basis" => "bases", + "diagnosis" => "diagnoses", + + "datum" => "data", + "medium" => "media", + "analysis" => "analyses", + + "node_child" => "node_children", + "child" => "children", + + "experience" => "experiences", + "day" => "days", + + "comment" => "comments", + "foobar" => "foobars", + "newsletter" => "newsletters", + + "old_news" => "old_news", + "news" => "news", + + "series" => "series", + "species" => "species", + + "quiz" => "quizzes", + + "perspective" => "perspectives", + + "ox" => "oxen", + "photo" => "photos", + "buffalo" => "buffaloes", + "tomato" => "tomatoes", + "dwarf" => "dwarves", + "elf" => "elves", + "information" => "information", + "equipment" => "equipment", + "bus" => "buses", + "status" => "statuses", + "status_code" => "status_codes", + "mouse" => "mice", + + "louse" => "lice", + "house" => "houses", + "octopus" => "octopi", + "virus" => "viri", + "alias" => "aliases", + "portfolio" => "portfolios", + + "vertex" => "vertices", + "matrix" => "matrices", + + "axis" => "axes", + "testis" => "testes", + "crisis" => "crises", + + "rice" => "rice", + "shoe" => "shoes", + + "horse" => "horses", + "prize" => "prizes", + "edge" => "edges" + } + + CamelToUnderscore = { + "Product" => "product", + "SpecialGuest" => "special_guest", + "ApplicationController" => "application_controller", + "Area51Controller" => "area51_controller" + } + + UnderscoreToLowerCamel = { + "product" => "product", + "special_guest" => "specialGuest", + "application_controller" => "applicationController", + "area51_controller" => "area51Controller" + } + + CamelToUnderscoreWithoutReverse = { + "HTMLTidy" => "html_tidy", + "HTMLTidyGenerator" => "html_tidy_generator", + "FreeBSD" => "free_bsd", + "HTML" => "html", + } + + CamelWithModuleToUnderscoreWithSlash = { + "Admin::Product" => "admin/product", + "Users::Commission::Department" => "users/commission/department", + "UsersSection::CommissionDepartment" => "users_section/commission_department", + } + + ClassNameToForeignKeyWithUnderscore = { + "Person" => "person_id", + "MyApplication::Billing::Account" => "account_id" + } + + ClassNameToForeignKeyWithoutUnderscore = { + "Person" => "personid", + "MyApplication::Billing::Account" => "accountid" + } + + ClassNameToTableName = { + "PrimarySpokesman" => "primary_spokesmen", + "NodeChild" => "node_children" + } + + UnderscoreToHuman = { + "employee_salary" => "Employee salary", + "employee_id" => "Employee", + "underground" => "Underground" + } + + MixtureToTitleCase = { + 'active_record' => 'Active Record', + 'ActiveRecord' => 'Active Record', + 'action web service' => 'Action Web Service', + 'Action Web Service' => 'Action Web Service', + 'Action web service' => 'Action Web Service', + 'actionwebservice' => 'Actionwebservice', + 'Actionwebservice' => 'Actionwebservice' + } + + OrdinalNumbers = { + "0" => "0th", + "1" => "1st", + "2" => "2nd", + "3" => "3rd", + "4" => "4th", + "5" => "5th", + "6" => "6th", + "7" => "7th", + "8" => "8th", + "9" => "9th", + "10" => "10th", + "11" => "11th", + "12" => "12th", + "13" => "13th", + "14" => "14th", + "20" => "20th", + "21" => "21st", + "22" => "22nd", + "23" => "23rd", + "24" => "24th", + "100" => "100th", + "101" => "101st", + "102" => "102nd", + "103" => "103rd", + "104" => "104th", + "110" => "110th", + "1000" => "1000th", + "1001" => "1001st" + } + + UnderscoresToDashes = { + "street" => "street", + "street_address" => "street-address", + "person_street_address" => "person-street-address" + } + + def test_pluralize_plurals + assert_equal "plurals", Inflector.pluralize("plurals") + assert_equal "Plurals", Inflector.pluralize("Plurals") + end + + SingularToPlural.each do |singular, plural| + define_method "test_pluralize_#{singular}" do + assert_equal(plural, Inflector.pluralize(singular)) + assert_equal(plural.capitalize, Inflector.pluralize(singular.capitalize)) + end + end + + SingularToPlural.each do |singular, plural| + define_method "test_singularize_#{plural}" do + assert_equal(singular, Inflector.singularize(plural)) + assert_equal(singular.capitalize, Inflector.singularize(plural.capitalize)) + end + end + + MixtureToTitleCase.each do |before, title_cased| + define_method 'test_titlecase' do + assert_equal(title_cased, Inflector.titleize(before)) + end + end + + def test_camelize + CamelToUnderscore.each do |camel, underscore| + assert_equal(camel, Inflector.camelize(underscore)) + end + end + + def test_underscore + CamelToUnderscore.each do |camel, underscore| + assert_equal(underscore, Inflector.underscore(camel)) + end + CamelToUnderscoreWithoutReverse.each do |camel, underscore| + assert_equal(underscore, Inflector.underscore(camel)) + end + end + + def test_camelize_with_module + CamelWithModuleToUnderscoreWithSlash.each do |camel, underscore| + assert_equal(camel, Inflector.camelize(underscore)) + end + end + + def test_underscore_with_slashes + CamelWithModuleToUnderscoreWithSlash.each do |camel, underscore| + assert_equal(underscore, Inflector.underscore(camel)) + end + end + + def test_demodulize + assert_equal "Account", Inflector.demodulize("MyApplication::Billing::Account") + end + + def test_foreign_key + ClassNameToForeignKeyWithUnderscore.each do |klass, foreign_key| + assert_equal(foreign_key, Inflector.foreign_key(klass)) + end + + ClassNameToForeignKeyWithoutUnderscore.each do |klass, foreign_key| + assert_equal(foreign_key, Inflector.foreign_key(klass, false)) + end + end + + def test_tableize + ClassNameToTableName.each do |class_name, table_name| + assert_equal(table_name, Inflector.tableize(class_name)) + end + end + + def test_classify + ClassNameToTableName.each do |class_name, table_name| + assert_equal(class_name, Inflector.classify(table_name)) + end + end + + def test_humanize + UnderscoreToHuman.each do |underscore, human| + assert_equal(human, Inflector.humanize(underscore)) + end + end + + def test_constantize + assert_equal Ace::Base::Case, Inflector.constantize("Ace::Base::Case") + assert_equal Ace::Base::Case, Inflector.constantize("::Ace::Base::Case") + assert_equal InflectorTest, Inflector.constantize("InflectorTest") + assert_equal InflectorTest, Inflector.constantize("::InflectorTest") + assert_raises(NameError) { Inflector.constantize("UnknownClass") } + assert_raises(NameError) { Inflector.constantize("An invalid string") } + end + + def test_constantize_doesnt_look_in_parent + assert_raises(NameError) { Inflector.constantize("Ace::Base::InflectorTest") } + end + + def test_ordinal + OrdinalNumbers.each do |number, ordinalized| + assert_equal(ordinalized, Inflector.ordinalize(number)) + end + end + + def test_dasherize + UnderscoresToDashes.each do |underscored, dasherized| + assert_equal(dasherized, Inflector.dasherize(underscored)) + end + end + + def test_underscore_as_reverse_of_dasherize + UnderscoresToDashes.each do |underscored, dasherized| + assert_equal(underscored, Inflector.underscore(Inflector.dasherize(underscored))) + end + end + + def test_underscore_to_lower_camel + UnderscoreToLowerCamel.each do |underscored, lower_camel| + assert_equal(lower_camel, Inflector.camelize(underscored, false)) + end + end +end diff --git a/vendor/rails/activesupport/test/json.rb b/vendor/rails/activesupport/test/json.rb new file mode 100644 index 00000000..fc4d7705 --- /dev/null +++ b/vendor/rails/activesupport/test/json.rb @@ -0,0 +1,57 @@ +$:.unshift File.dirname(__FILE__) + '/../lib' +require 'active_support' +require 'test/unit' + +class Foo + def initialize(a, b) + @a, @b = a, b + end +end + +class TestJSONEmitters < Test::Unit::TestCase + TrueTests = [[ true, %(true) ]] + FalseTests = [[ false, %(false) ]] + NilTests = [[ nil, %(null) ]] + NumericTests = [[ 1, %(1) ], + [ 2.5, %(2.5) ]] + + StringTests = [[ 'this is the string', %("this is the string") ], + [ 'a "string" with quotes', %("a \\"string\\" with quotes") ]] + + ArrayTests = [[ ['a', 'b', 'c'], %([\"a\", \"b\", \"c\"]) ], + [ [1, 'a', :b, nil, false], %([1, \"a\", \"b\", null, false]) ]] + + HashTests = [[ {:a => :b, :c => :d}, %({\"c\": \"d\", \"a\": \"b\"}) ]] + + SymbolTests = [[ :a, %("a") ], + [ :this, %("this") ], + [ :"a b", %("a b") ]] + + ObjectTests = [[ Foo.new(1, 2), %({\"a\": 1, \"b\": 2}) ]] + + VariableTests = [[ ActiveSupport::JSON::Variable.new('foo'), 'foo'], + [ ActiveSupport::JSON::Variable.new('alert("foo")'), 'alert("foo")']] + RegexpTests = [[ /^a/, '/^a/' ], /^\w{1,2}[a-z]+/ix, '/^\\w{1,2}[a-z]+/ix'] + + constants.grep(/Tests$/).each do |class_tests| + define_method("test_#{class_tests[0..-6].downcase}") do + self.class.const_get(class_tests).each do |pair| + assert_equal pair.last, pair.first.to_json + end + end + end + + def test_utf8_string_encoded_properly_when_kcode_is_utf8 + old_kcode, $KCODE = $KCODE, 'UTF8' + assert_equal '"\\u20ac2.99"', '€2.99'.to_json + assert_equal '"\\u270e\\u263a"', '✎☺'.to_json + ensure + $KCODE = old_kcode + end + + def test_exception_raised_when_encoding_circular_reference + a = [1] + a << a + assert_raises(ActiveSupport::JSON::CircularReferenceError) { a.to_json } + end +end diff --git a/vendor/rails/activesupport/test/option_merger_test.rb b/vendor/rails/activesupport/test/option_merger_test.rb new file mode 100644 index 00000000..f5287d57 --- /dev/null +++ b/vendor/rails/activesupport/test/option_merger_test.rb @@ -0,0 +1,34 @@ +require 'test/unit' + +unless defined? ActiveSupport::OptionMerger + require File.dirname(__FILE__) + '/../lib/active_support/option_merger' + require File.dirname(__FILE__) + '/../lib/active_support/core_ext/object' +end + +class OptionMergerTest < Test::Unit::TestCase + def setup + @options = {:hello => 'world'} + end + + def test_method_with_options_merges_options_when_options_are_present + local_options = {:cool => true} + + with_options(@options) do |o| + assert_equal local_options, method_with_options(local_options) + assert_equal @options.merge(local_options), + o.method_with_options(local_options) + end + end + + def test_method_with_options_appends_options_when_options_are_missing + with_options(@options) do |o| + assert_equal Hash.new, method_with_options + assert_equal @options, o.method_with_options + end + end + + private + def method_with_options(options = {}) + options + end +end diff --git a/vendor/rails/activesupport/test/ordered_options_test.rb b/vendor/rails/activesupport/test/ordered_options_test.rb new file mode 100644 index 00000000..0247331c --- /dev/null +++ b/vendor/rails/activesupport/test/ordered_options_test.rb @@ -0,0 +1,55 @@ +require 'test/unit' + +require File.dirname(__FILE__) + '/../lib/active_support/ordered_options' + +class OrderedOptionsTest < Test::Unit::TestCase + def test_usage + a = OrderedOptions.new + + assert_nil a[:not_set] + + a[:allow_concurreny] = true + assert_equal 1, a.size + assert a[:allow_concurreny] + + a[:allow_concurreny] = false + assert_equal 1, a.size + assert !a[:allow_concurreny] + + a["else_where"] = 56 + assert_equal 2, a.size + assert_equal 56, a[:else_where] + end + + def test_looping + a = OrderedOptions.new + + a[:allow_concurreny] = true + a["else_where"] = 56 + + test = [[:allow_concurreny, true], [:else_where, 56]] + + a.each_with_index do |(key, value), index| + assert_equal test[index].first, key + assert_equal test[index].last, value + end + end + + def test_method_access + a = OrderedOptions.new + + assert_nil a.not_set + + a.allow_concurreny = true + assert_equal 1, a.size + assert a.allow_concurreny + + a.allow_concurreny = false + assert_equal 1, a.size + assert !a.allow_concurreny + + a.else_where = 56 + assert_equal 2, a.size + assert_equal 56, a.else_where + end +end diff --git a/vendor/rails/activesupport/test/reloadable_test.rb b/vendor/rails/activesupport/test/reloadable_test.rb new file mode 100644 index 00000000..1b147e0d --- /dev/null +++ b/vendor/rails/activesupport/test/reloadable_test.rb @@ -0,0 +1,84 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../lib/active_support/core_ext/class' +require File.dirname(__FILE__) + '/../lib/active_support/core_ext/module' +require File.dirname(__FILE__) + '/../lib/active_support/reloadable' + +module ReloadableTestSandbox + + class AReloadableClass + include Reloadable + end + class AReloadableClassWithSubclasses + include Reloadable + end + class AReloadableSubclass < AReloadableClassWithSubclasses + end + class ANonReloadableSubclass < AReloadableClassWithSubclasses + def self.reloadable? + false + end + end + class AClassWhichDefinesItsOwnReloadable + def self.reloadable? + 10 + end + include Reloadable + end + + class SubclassesReloadable + include Reloadable::Subclasses + end + class ASubclassOfSubclassesReloadable < SubclassesReloadable + end + + class AnOnlySubclassReloadableClassSubclassingAReloadableClass + include Reloadable::Subclasses + end + + class ASubclassofAOnlySubclassReloadableClassWhichWasSubclassingAReloadableClass < AnOnlySubclassReloadableClassSubclassingAReloadableClass + end +end + +class ReloadableTest < Test::Unit::TestCase + def test_classes_receive_reloadable + assert ReloadableTestSandbox::AReloadableClass.respond_to?(:reloadable?) + end + def test_classes_inherit_reloadable + assert ReloadableTestSandbox::AReloadableSubclass.respond_to?(:reloadable?) + end + def test_reloadable_is_not_overwritten_if_present + assert_equal 10, ReloadableTestSandbox::AClassWhichDefinesItsOwnReloadable.reloadable? + end + + def test_only_subclass_reloadable + assert ! ReloadableTestSandbox::SubclassesReloadable.reloadable? + assert ReloadableTestSandbox::ASubclassOfSubclassesReloadable.reloadable? + end + + def test_inside_hierarchy_only_subclass_reloadable + assert ! ReloadableTestSandbox::AnOnlySubclassReloadableClassSubclassingAReloadableClass.reloadable? + assert ReloadableTestSandbox::ASubclassofAOnlySubclassReloadableClassWhichWasSubclassingAReloadableClass.reloadable? + end + + def test_removable_classes + reloadables = %w( + AReloadableClass + AReloadableClassWithSubclasses + AReloadableSubclass + AClassWhichDefinesItsOwnReloadable + ASubclassOfSubclassesReloadable + ) + non_reloadables = %w( + ANonReloadableSubclass + SubclassesReloadable + ) + + results = Reloadable.reloadable_classes + reloadables.each do |name| + assert results.include?(ReloadableTestSandbox.const_get(name)), "Expected #{name} to be reloadable" + end + non_reloadables.each do |name| + assert ! results.include?(ReloadableTestSandbox.const_get(name)), "Expected #{name} NOT to be reloadable" + end + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/test/time_zone_test.rb b/vendor/rails/activesupport/test/time_zone_test.rb new file mode 100644 index 00000000..3e35c327 --- /dev/null +++ b/vendor/rails/activesupport/test/time_zone_test.rb @@ -0,0 +1,92 @@ +require 'test/unit' +require File.dirname(__FILE__)+'/../lib/active_support/values/time_zone' + +class TimeZoneTest < Test::Unit::TestCase + class MockTime + def self.now + Time.utc( 2004, 7, 25, 14, 49, 00 ) + end + + def self.local(*args) + Time.utc(*args) + end + end + + TimeZone::Time = MockTime + + def test_formatted_offset_positive + zone = TimeZone.create( "Test", 4200 ) + assert_equal "+01:10", zone.formatted_offset + end + + def test_formatted_offset_negative + zone = TimeZone.create( "Test", -4200 ) + assert_equal "-01:10", zone.formatted_offset + end + + def test_now + zone = TimeZone.create( "Test", 4200 ) + assert_equal Time.local(2004,7,25,15,59,00).to_a[0,6], zone.now.to_a[0,6] + end + + def test_today + zone = TimeZone.create( "Test", 43200 ) + assert_equal Date.new(2004,7,26), zone.today + end + + def test_adjust_negative + zone = TimeZone.create( "Test", -4200 ) + assert_equal Time.utc(2004,7,24,23,55,0), zone.adjust(Time.utc(2004,7,25,1,5,0)) + end + + def test_adjust_positive + zone = TimeZone.create( "Test", 4200 ) + assert_equal Time.utc(2004,7,26,1,5,0), zone.adjust(Time.utc(2004,7,25,23,55,0)) + end + + def test_unadjust + zone = TimeZone.create( "Test", 4200 ) + expect = Time.utc(2004,7,24,23,55,0).to_a[0,6] + actual = zone.unadjust(Time.utc(2004,7,25,1,5,0)).to_a[0,6] + assert_equal expect, actual + end + + def test_zone_compare + zone1 = TimeZone.create( "Test1", 4200 ) + zone2 = TimeZone.create( "Test1", 5600 ) + assert zone1 < zone2 + assert zone2 > zone1 + + zone1 = TimeZone.create( "Able", 10000 ) + zone2 = TimeZone.create( "Zone", 10000 ) + assert zone1 < zone2 + assert zone2 > zone1 + + zone1 = TimeZone.create( "Able", 10000 ) + assert zone1 == zone1 + end + + def test_to_s + zone = TimeZone.create( "Test", 4200 ) + assert_equal "(GMT+01:10) Test", zone.to_s + end + + def test_all_sorted + all = TimeZone.all + 1.upto( all.length-1 ) do |i| + assert all[i-1] < all[i] + end + end + + def test_index + assert_nil TimeZone["bogus"] + assert_not_nil TimeZone["Central Time (US & Canada)"] + end + + def test_new + a = TimeZone.new("Berlin") + b = TimeZone.new("Berlin") + assert_same a, b + assert_nil TimeZone.new("bogus") + end +end diff --git a/vendor/rails/activesupport/test/whiny_nil_test.rb b/vendor/rails/activesupport/test/whiny_nil_test.rb new file mode 100644 index 00000000..9ed31bfe --- /dev/null +++ b/vendor/rails/activesupport/test/whiny_nil_test.rb @@ -0,0 +1,40 @@ +require 'test/unit' + +# mock to enable testing without activerecord +module ActiveRecord + class Base + def save! + end + end +end + +require File.dirname(__FILE__) + '/../lib/active_support/inflector' +require File.dirname(__FILE__) + '/../lib/active_support/whiny_nil' + +class WhinyNilTest < Test::Unit::TestCase + def test_unchanged + nil.method_thats_not_in_whiners + rescue NoMethodError => nme + assert_match(/nil.method_thats_not_in_whiners/, nme.message) + end + + def test_active_record + nil.save! + rescue NoMethodError => nme + assert(!(nme.message =~ /nil:NilClass/)) + assert_match(/nil\.save!/, nme.message) + end + + def test_array + nil.each + rescue NoMethodError => nme + assert(!(nme.message =~ /nil:NilClass/)) + assert_match(/nil\.each/, nme.message) + end + + def test_id + nil.id + rescue RuntimeError => nme + assert(!(nme.message =~ /nil:NilClass/)) + end +end diff --git a/vendor/rails/cleanlogs.sh b/vendor/rails/cleanlogs.sh new file mode 100755 index 00000000..a1974410 --- /dev/null +++ b/vendor/rails/cleanlogs.sh @@ -0,0 +1 @@ +rm activerecord/debug.log activerecord/test/debug.log actionpack/debug.log diff --git a/vendor/rails/pushgems.rb b/vendor/rails/pushgems.rb new file mode 100755 index 00000000..a8b516fb --- /dev/null +++ b/vendor/rails/pushgems.rb @@ -0,0 +1,14 @@ +#!/usr/local/bin/ruby + +unless ARGV.first == "no_build" + build_number = build_number = `svn log -q -rhead http://dev.rubyonrails.org/svn/rails`.scan(/r([0-9]*)/).first.first.to_i +end + +%w( actionwebservice actionmailer actionpack activerecord railties activesupport ).each do |pkg| + puts "Pushing: #{pkg} (#{build_number})" + if build_number + `cd #{pkg} && rm -rf pkg && PKG_BUILD=#{build_number} rake pgem && cd ..` + else + `cd #{pkg} && rm -rf pkg && rake pgem && cd ..` + end +end diff --git a/vendor/rails/railties/CHANGELOG b/vendor/rails/railties/CHANGELOG new file mode 100644 index 00000000..d6947a29 --- /dev/null +++ b/vendor/rails/railties/CHANGELOG @@ -0,0 +1,1080 @@ +*1.1.6* (August 10th, 2006) + +* Additional security patch + + +*1.1.5* (August 8th, 2006) + +* Mention in docs that config.frameworks doesn't work when getting Rails via Gems. #4857 [Alisdair McDiarmid] + +* Change the scaffolding layout to use yield rather than @content_for_layout. [Marcel Molina Jr.] + +* Includes critical security patch + + +*1.1.4* (June 29th, 2006) + +* Remove use of opts.on { |options[:name] } style hash assignment. References #4440. [headius@headius.com] + +* Updated to Action Pack 1.12.3, ActionWebService 1.1.4, ActionMailer 1.2.3 + + +*1.1.3* (June 27th, 2006) + +* Updated to Active Record 1.14.3, Action Pack 1.12.2, ActionWebService 1.1.3, ActionMailer 1.2.2 + + +*1.1.2* (April 9th, 2006) + +* Added rake rails:update:configs to update config/boot.rb from the latest (also included in rake rails:update) [DHH] + +* Fixed that boot.rb would set RAILS_GEM_VERSION twice, not respect an uncommented RAILS_GEM_VERSION line, and not use require_gem [DHH] + + +*1.1.1* (April 6th, 2006) + +* Enhances plugin#discover allowing it to discover svn:// like URIs (closes #4565) [ruben.nine@gmail.com] + +* Update to Prototype 1.5.0_rc0 [Sam Stephenson] + +* Fixed that the -r/--ruby path option of the rails command was not being respected #4549 [ryan.raaum@gmail.com] + +* Added that Dispatcher exceptions should not be shown to the user unless a default log has not been configured. Instead show public/500.html [DHH] + +* Fixed that rake clone_structure_to_test should quit on pgsql if the dump is unsuccesful #4585 [augustz@augustz.com] + +* Fixed that rails --version should have the return code of 0 (success) #4560 [blair@orcaware.com] + +* Install alias so Rails::InfoController is accessible at /rails_info. Closes #4546. [Nicholas Seckar] + +* Fixed that spawner should daemonize if running in repeat mode [DHH] + +* Added TAG option for rake rails:freeze:edge, so you can say rake rails:freeze:edge TAG=rel_1-1-0 to lock to the 1.1.0 release [DHH] + +* Applied Prototype $() performance patches (#4465, #4477) and updated script.aculo.us [Sam Stephenson, Thomas Fuchs] + +* Use --simple-prompt instead of --prompt-mode simple for console compatibility with Windows/Ruby 1.8.2 #4532 [starr@starrnhorne.com] + +* Make Rails::VERSION implicitly loadable #4491. [Nicholas Seckar] + +* Fixed rake rails:freeze:gems #4518 [benji@silverinsanity.com] + +* Added -f/--freeze option to rails command for freezing the application to the Rails version it was generated with [DHH] + +* Added gem binding of apps generated through the rails command to the gems of they were generated with [Nicholas Seckar] + +* Added expiration settings for JavaScript, CSS, HTML, and images to default lighttpd.conf [DHH] + +* Added gzip compression for JavaScript, CSS, and HTML to default lighttpd.conf [DHH] + +* Avoid passing escapeHTML non-string in Rails' info controller [Nicholas Seckar] + + +*1.1.0* (March 27th, 2006) + +* Allow db:fixtures:load to load a subset of the applications fixtures. [Chad Fowler] + + ex. + + rake db:fixtures:load FIXTURES=customers,plans + +* Update to Prototype 1.5.0_pre1 [Sam Stephenson] + +* Update to script.aculo.us 1.6 [Thomas Fuchs] + +* Add an integration_test generator [Jamis Buck] + +* Make all ActionView helpers available in the console from the helper method for debugging purposes. n.b.: Only an 80% solution. Some stuff won't work, most will. [Marcel Molina Jr.] + + ex. + + >> puts helper.options_for_select([%w(a 1), %w(b 2), %w(c 3)]) + + + + => nil + +* Replaced old session rake tasks with db:sessions:create to generate a migration, and db:sessions:clear to remove sessions. [Rick Olson] + +* Reject Ruby 1.8.3 when loading Rails; extract version checking code. [Chad Fowler] + +* Remove explicit loading of RailsInfo and RailsInfoController. [Nicholas Seckar] + +* Move RailsInfo and RailsInfoController to Rails::Info and Rails::InfoController. [Nicholas Seckar] + +* Extend load path with Railties' builtin directory to make adding support code easy. [Nicholas Seckar] + +* Fix the rails_info controller by explicitly loading it, and marking it as not reloadable. [Nicholas Seckar] + +* Fixed rails:freeze:gems for Windows #3274 [paul@paulbutcher.com] + +* Added 'port open?' check to the spawner when running in repeat mode so we don't needlessly boot the dispatcher if the port is already in use anyway #4089 [guy.naor@famundo.com] + +* Add verification to generated scaffolds, don't allow get for unsafe actions [Michael Koziarski] + +* Don't replace application.js in public/javascripts if it already exists [Cody Fauser] + +* Change test:uncommitted to delay execution of `svn status` by using internal Rake API's. [Nicholas Seckar] + +* Use require_library_or_gem to load rake in commands/server.rb. Closes #4205. [rob.rasmussen@gmail.com] + +* Use the Rake API instead of shelling out to create the tmp directory in commands/server.rb. [Chad Fowler] + +* Added a backtrace to the evil WSOD (White Screen of Death). Closes #4073. TODO: Clearer exceptions [Rick Olson] + +* Added tracking of database and framework versions in script/about #4088 [charles.gerungan@gmail.com/Rick Olson] + +* Added public/javascripts/application.js as a sample since it'll automatically be included in javascript_include_tag :defaults [DHH] + +* Added socket cleanup for lighttpd, both before and after [DHH] + +* Added automatic creation of tmp/ when running script/server [DHH] + +* Added silence_stream that'll work on both STDERR or STDOUT or any other stream and deprecated silence_stderr in the process [DHH] + +* Added reload! method to script/console to reload all models and others that include Reloadable without quitting the console #4056 [esad@esse.at] + +* Added that rake rails:freeze:edge will now just export all the contents of the frameworks instead of just lib, so stuff like rails:update:scripts, rails:update:javascripts, and script/server on lighttpd still just works #4047 [DHH] + +* Added fix for upload problems with lighttpd from Safari/IE to config/lighttpd.conf #3999 [thijs@fngtps.com] + +* Added test:uncommitted to test changes since last checkin to Subversion #4035 [technomancy@gmail.com] + +* Help script/about print the correct svn revision when in a non-English locale. #4026 [babie7a0@ybb.ne.jp] + +* Add 'app' accessor to script/console as an instance of Integration::Session [Jamis Buck] + +* Generator::Base#usage takes an optional message argument which defaults to Generator::Base#usage_message. [Jeremy Kemper] + +* Remove the extraneous AR::Base.threaded_connections setting from the webrick server. [Jeremy Kemper] + +* Add integration test support to app generation and testing [Jamis Buck] + +* Added namespaces to all tasks, so for example load_fixtures is now db:fixtures:load. All the old task names are still valid, they just point to the new namespaced names. "rake -T" will only show the namespaced ones, though [DHH] + +* CHANGED DEFAULT: ActiveRecord::Base.schema_format is now :ruby by default instead of :sql. This means that we'll assume you want to live in the world of db/schema.rb where the grass is green and the girls are pretty. If your schema contains un-dumpable elements, such as constraints or database-specific column types, you just got an invitation to either 1) patch the dumper to include foreign key support, 2) stop being db specific, or 3) just change the default in config/environment.rb to config.active_record.schema_format = :sql -- we even include an example for that on new Rails skeletons now. Brought to you by the federation of opinionated framework builders! [DHH] + +* Added -r/--repeat option to script/process/spawner that offers the same loop protection as the spinner did. This deprecates the script/process/spinner, so it's no longer included in the default Rails skeleton, but still available for backwards compatibility #3461 [ror@andreas-s.net] + +* Added collision option to template generation in generators #3329 [anna@wota.jp]. Examples: + + m.template "stuff.config" , "config/stuff.config" , :collision => :skip + m.template "auto-stamping", "config/generator.log", :collision => :force + +* Added more information to script/plugin's doings to ease debugging #3755 [Rick Olson] + +* Changed the default configuration for lighttpd to use tmp/sockets instead of log/ for the FastCGI sockets [DHH] + +* Added a default configuration of the FileStore for fragment caching if tmp/cache is available, which makes action/fragment caching ready to use out of the box with no additional configuration [DHH] + +* Changed the default session configuration to place sessions in tmp/sessions, if that directory is available, instead of /tmp (this essentially means a goodbye to 9/10 White Screen of Death errors and should have web hosting firms around the world cheering) [DHH] + +* Added tmp/sessions, tmp/cache, and tmp/sockets as default directories in the Rails skeleton [DHH] + +* Added that script/generate model will now automatically create a migration file for the model created. This can be turned off by calling the generator with --skip-migration [DHH] + +* Added -d/--database option to the rails command, so you can do "rails --database=sqlite2 myapp" to start a new application preconfigured to use SQLite2 as the database. Removed the configuration examples from SQLite and PostgreSQL from the default MySQL configuration [DHH] + +* Allow script/server -c /path/to/lighttpd.conf [Jeremy Kemper] + +* Remove hardcoded path to reaper script in script/server [Jeremy Kemper] + +* Update script.aculo.us to V1.5.3 [Thomas Fuchs] + +* Added SIGTRAP signal handler to RailsFCGIHandler that'll force the process into a breakpoint after the next request. This breakpoint can then be caught with script/breakpointer and give you access to the Ruby image inside that process. Useful for debugging memory leaks among other things [DHH] + +* Changed default lighttpd.conf to use CWD from lighttpd 1.4.10 that allows the same configuration to be used for both detach and not. Also ensured that auto-repeaping of FCGIs only happens when lighttpd is not detached. [DHH] + +* Added Configuration#after_initialize for registering a block which gets called after the framework is fully initialized. Useful for things like per-environment configuration of plugins. [Michael Koziarski] + +* Added check for RAILS_FRAMEWORK_ROOT constant that allows the Rails framework to be found in a different place than vendor/rails. Should be set in boot.rb. [DHH] + +* Fixed that static requests could unlock the mutex guarding dynamic requests in the WEBrick servlet #3433 [tom@craz8.com] + +* Fixed documentation tasks to work with Rake 0.7.0 #3563 [kazuhiko@fdiary.net] + +* Update to Prototype 1.5.0_pre0 [Sam Stephenson] + +* Sort the list of plugins so we load in a consistent order [Rick Olson] + +* Show usage when script/plugin is called without arguments [tom@craz8.com] + +* Corrected problems with plugin loader where plugins set 'name' incorrectly #3297 [anna@wota.jp] + +* Make migration generator only report on exact duplicate names, not partial dupliate names. #3442 [jeremy@planetargon.com Marcel Molina Jr.] + +* Fix typo in mailer generator USAGE. #3458 [chriztian.steinmeier@gmail.com] + +* Ignore version mismatch between pg_dump and the database server. #3457 [simon.stapleton@gmail.com] + +* Reap FCGI processes after lighttpd exits. [Sam Stephenson] + +* Honor ActiveRecord::Base.pluralize_table_names when creating and destroying session store table. #3204. [rails@bencurtis.com, Marcel Molina Jr.] + + +*1.0.0* (December 13th, 2005) + +* Update instructions on how to find and install generators. #3172. [Chad Fowler] + +* Generator looks in vendor/generators also. [Chad Fowler] + +* Generator copies files in binary mode. #3156 [minimudboy@gmail.com] + +* Add builtin/ to the gemspec. Closes #3047. [Nicholas Seckar, Sam Stephenson] + +* Add install.rb file to plugin generation which is loaded, if it exists, when you install a plugin. [Marcel Molina Jr.] + +* Run initialize_logger in script/lighttpd to ensure the log file exists before tailing it. [Sam Stephenson] + +* Make load_fixtures include csv fixtures. #3053. [me@mdaines.com] + +* Fix freeze_gems so that the latest rails version is dumped by default. [Nicholas Seckar] + +* script/plugin: handle root paths and plugin names which contain spaces. #2995 [justin@aspect.net] + +* Model generator: correct relative path to test_helper in unit test. [Jeremy Kemper] + +* Make the db_schema_dump task honor the SCHEMA environment variable if present the way db_schema_import does. #2931. [Blair Zajac] + +* Have the lighttpd server script report the actual ip to which the server is bound. #2903. [Adam] + +* Add plugin library directories to the load path after the lib directory so that libraries in the lib directory get precedence. #2910. [james.adam@gmail.com] + +* Make help for the console command more explicit about how to specify the desired environment in which to run the console. #2911. [anonymous] + +* PostgreSQL: the purge_test_database Rake task shouldn't explicitly specify the template0 template when creating a fresh test database. #2964 [dreamer3@gmail.com] + +* Introducing the session_migration generator. Creates an add_session_table migration. Allows generator to specify migrations directory. #2958, #2960 [Rick Olson] + +* script/console uses RAILS_ENV environment variable if present. #2932 [Blair Zajac + +* Windows: eliminate the socket option in database.yml. #2924 [Wayne Vucenic ] + +* Eliminate nil from newly generated logfiles. #2927 [Blair Zajac ] + +* Rename Version constant to VERSION. #2802 [Marcel Molina Jr.] + +* Eliminate Subversion dependencies in scripts/plugin. Correct install options. Introduce --force option to reinstall a plugin. Remove useless --long option for list. Use --quiet to quiet the download output and --revision to update to a specific svn revision. #2842 [Chad Fowler, Rick Olson] + +* SQLite: the clone_structure_to_test and purge_test_database Rake tasks should always use the test environment. #2846 [Rick Olson] + +* Make sure that legacy db tasks also reference :database for SQLite #2830 [kazuhiko@fdiary.net] + +* Pass __FILE__ when evaluating plugins' init.rb. #2817 [james.adam@gmail.com] + +* Better svn status matching for generators. #2814 [François Beausoleil , Blair Zajac ] + +* Don't reload routes until plugins have been loaded so they have a chance to extend the routing capabilities [DHH] + +* Don't detach or fork for script/server tailing [Nicholas Seckar] + +* Changed all script/* to use #!/usr/bin/env ruby instead of hard-coded Ruby path. public/dispatcher.* still uses the hard-coded path for compatibility with web servers that don't have Ruby in path [DHH] + +* Force RAILS_ENV to be "test" when running tests, so that ENV["RAILS_ENV"] = "production" in config/environment.rb doesn't wreck havok [DHH] #2660 + +* Correct versioning in :freeze_gems Rake task. #2778 [jakob@mentalized.net, Jeremy Kemper] + +* Added an omnipresent RailsInfoController with a properties action that delivers an HTML rendering of Rails::Info (but only when local_request? is true). Added a new default index.html which fetches this with Ajax. [Sam Stephenson] + + +*0.14.3 (RC4)* (November 7th, 2005) + +* Add 'add_new_scripts' rake task for adding new rails scripts to script/* [Jamis Buck] + +* Remove bogus hyphen from script/process/reaper calls to 'ps'. #2767 [anonymous] + +* Copy lighttpd.conf when it is first needed, instead of on app creation [Jamis Buck] + +* Use require_library_or_gem 'fcgi' in script/server [Sam Stephenson] + +* Added default lighttpd config in config/lighttpd.conf and added a default runner for lighttpd in script/server (works like script/server, but using lighttpd and FastCGI). It will use lighttpd if available, otherwise WEBrick. You can force either or using 'script/server lighttpd' or 'script/server webrick' [DHH] + +* New configuration option config.plugin_paths which may be a single path like the default 'vendor/plugins' or an array of paths: ['vendor/plugins', 'lib/plugins']. [Jeremy Kemper] + +* Plugins are discovered in nested paths, so you can organize your plugins directory as you like. [Jeremy Kemper] + +* Refactor load_plugin from load_plugins. #2757 [alex.r.moon@gmail.com] + +* Make use of silence_stderr in script/lighttpd, script/plugin, and Rails::Info [Sam Stephenson] + +* Enable HTTP installation of plugins when svn isn't avaialable. Closes #2661. [Chad Fowler] + +* Added script/about to display formatted Rails::Info output [Sam Stephenson] + +* Added Rails::Info to catalog assorted information about a Rails application's environment [Sam Stephenson] + +* Tail the logfile when running script/server lighttpd in the foreground [Sam Stephenson] + +* Try to guess the port number from config/lighttpd.conf in script/server lighttpd [Sam Stephenson] + +* Don't reap spawn-fcgi. #2727 [matthew@walker.wattle.id.au] + +* Reaper knows how to find processes even if the dispatch path is very long. #2711 [matthew@walker.wattle.id.au] + +* Make fcgi handler respond to TERM signals with an explicit exit [Jamis Buck] + +* Added demonstration of fixture use to the test case generated by the model generator [DHH] + +* If specified, pass PostgreSQL client character encoding to createdb. #2703 [Kazuhiko ] + +* Catch CGI multipart parse errors. Wrap dispatcher internals in a failsafe response handler. [Jeremy Kemper] + +* The freeze_gems Rake task accepts the VERSION environment variable to decide which version of Rails to pull into vendor/rails. [Chad Fowler, Jeremy Kemper] + +* Removed script.aculo.us.js, builder.js and slider.js (preperation for move of scriptaculous extensions to plugins, core scriptaculous will remain in Railties) [Thomas Fuchs] + +* The freeze_edge Rake task does smarter svn detection and can export a specific revision by passing the REVISION environment variable. For example: rake freeze_edge REVISION=1234. #2663 [Rick Olson] + +* Comment database.yml and include PostgreSQL and SQLite examples. [Jeremy Kemper] + +* Improve script/plugin on Windows. #2646 [Chad Fowler] + +* The *_plugindoc Rake tasks look deeper into the plugins' lib directories. #2652 [bellis@deepthought.org] + +* The PostgreSQL :db_structure_dump Rake task limits its dump to the schema search path in database.yml. [Anatol Pomozov ] + +* Add task to generate rdoc for all installed plugins. [Marcel Molina] + +* Update script.aculo.us to V1.5_rc4 [Thomas Fuchs] + +* Add default Mac + DarwinPorts MySQL socket locations to the app generator. [Jeremy Kemper] + +* Migrations may be destroyed: script/destroy migration foo. #2635 [Charles M. Gerungan , Jamis Buck, Jeremy Kemper] + +* Added that plugins can carry generators and that generator stub files can be created along with new plugins using script/generate plugin --with-generator [DHH] + +* Removed app/apis as a default empty dir since its automatically created when using script/generate web_service [DHH] + +* Added script/plugin to manage plugins (install, remove, list, etc) [Ryan Tomayko] + +* Added test_plugins task: Run the plugin tests in vendor/plugins/**/test (or specify with PLUGIN=name) [DHH] + +* Added plugin generator to create a stub structure for a new plugin in vendor/plugins (see "script/generate plugin" for help) [DHH] + +* Fixed scaffold generator when started with only 1 parameter #2609 [self@mattmower.com] + +* rake should run functional tests even if the unit tests have failures [Jim Weirich] + +* Back off cleanpath to be symlink friendly. Closes #2533 [Nicholas Seckar] + +* Load rake task files in alphabetical order so you can build dependencies and count on them #2554 [Blair Zajac] + + +*0.14.2 (RC3)* (October 26th, 2005) + +* Constants set in the development/test/production environment file are set in Object + +* Scaffold generator pays attention to the controller name. #2562 [self@mattmower.com] + +* Include tasks from vendor/plugins/*/tasks in the Rakefile #2545 [Rick Olson] + + +*0.14.1 (RC2)* (October 19th, 2005) + +* Don't clean RAILS_ROOT on windows + +* Remove trailing '/' from RAILS_ROOT [Nicholas Seckar] + +* Upgraded to Active Record 1.12.1 and Action Pack 1.10.1 + + +*0.14.0 (RC1)* (October 16th, 2005) + +* Moved generator folder from RAILS_ROOT/generators to RAILS_ROOT/lib/generators [Tobias Luetke] + +* Fix rake dev and related commands [Nicholas Seckar] + +* The rails command tries to deduce your MySQL socket by running `mysql_config +--socket`. If it fails, default to /path/to/your/mysql.sock + +* Made the rails command use the application name for database names in the tailored database.yml file. Example: "rails ~/projects/blog" will use "blog_development" instead of "rails_development". [Florian Weber] + +* Added Rails framework freezing tasks: freeze_gems (freeze to current gems), freeze_edge (freeze to Rails SVN trunk), unfreeze_rails (float with newest gems on system) + +* Added update_javascripts task which will fetch all the latest js files from your current rails install. Use after updating rails. [Tobias Luetke] + +* Added cleaning of RAILS_ROOT to useless elements such as '../non-dot-dot/'. Provides cleaner backtraces and error messages. [Nicholas Seckar] + +* Made the instantiated/transactional fixtures settings be controlled through Rails::Initializer. Transactional and non-instantiated fixtures are default from now on. [Florian Weber] + +* Support using different database adapters for development and test with ActiveRecord::Base.schema_format = :ruby [Sam Stephenson] + +* Make webrick work with session(:off) + +* Add --version, -v option to the Rails command. Closes #1840. [stancell] + +* Update Prototype to V1.4.0_pre11, script.aculo.us to V1.5_rc3 [2504] and fix the rails generator to include the new .js files [Thomas Fuchs] + +* Make the generator skip a file if it already exists and is identical to the new file. + +* Add experimental plugin support #2335 + +* Made Rakefile aware of new .js files in script.aculo.us [Thomas Fuchs] + +* Make table_name and controller_name in generators honor AR::Base.pluralize_table_names. #1216 #2213 [kazuhiko@fdiary.net] + +* Clearly label functional and unit tests in rake stats output. #2297 [lasse.koskela@gmail.com] + +* Make the migration generator only check files ending in *.rb when calculating the next file name #2317 [Chad Fowler] + +* Added prevention of duplicate migrations from the generator #2240 [fbeausoleil@ftml.net] + +* Add db_schema_dump and db_schema_import rake tasks to work with the new ActiveRecord::SchemaDumper (for dumping a schema to and reading a schema from a ruby file). + +* Reformed all the config/environments/* files to conform to the new Rails::Configuration approach. Fully backwards compatible. + +* Added create_sessions_table, drop_sessions_table, and purge_sessions_table as rake tasks for databases that supports migrations (MySQL, PostgreSQL, SQLite) to get a table for use with CGI::Session::ActiveRecordStore + +* Added dump of schema version to the db_structure_dump task for databases that support migrations #1835 [Rick Olson] + +* Fixed script/profiler for Ruby 1.8.2 #1863 [Rick Olson] + +* Fixed clone_structure_to_test task for SQLite #1864 [jon@burningbush.us] + +* Added -m/--mime-types option to the WEBrick server, so you can specify a Apache-style mime.types file to load #2059 [ask@develooper.com] + +* Added -c/--svn option to the generator that'll add new files and remove destroyed files using svn add/revert/remove as appropriate #2064 [kevin.clark@gmail.com] + +* Added -c/--charset option to WEBrick server, so you can specify a default charset (which without changes is UTF-8) #2084 [wejn@box.cz] + +* Make the default stats task extendable by modifying the STATS_DIRECTORIES constant + +* Allow the selected environment to define RAILS_DEFAULT_LOGGER, and have Rails::Initializer use it if it exists. + +* Moved all the shared tasks from Rakefile into Rails, so that the Rakefile is empty and doesn't require updating. + +* Added Rails::Initializer and Rails::Configuration to abstract all of the common setup out of config/environment.rb (uses config/boot.rb to bootstrap the initializer and paths) + +* Fixed the scaffold generator to fail right away if the database isn't accessible instead of in mid-air #1169 [Chad Fowler] + +* Corrected project-local generator location in scripts.rb #2010 [Michael Schuerig] + +* Don't require the environment just to clear the logs #2093 [Scott Barron] + +* Make the default rakefile read *.rake files from config/tasks (for easy extension of the rakefile by e.g. generators) + +* Only load breakpoint in development mode and when BREAKPOINT_SERVER_PORT is defined. + +* Allow the --toggle-spin switch on process/reaper to be negated + +* Replace render_partial with render :partial in scaffold generator [Nicholas Seckar] + +* Added -w flag to ps in process/reaper #1934 [Scott Barron] + +* Allow ERb in the database.yml file (just like with fixtures), so you can pull out the database configuration in environment variables #1822 [Duane Johnson] + +* Added convenience controls for FCGI processes (especially when managed remotely): spinner, spawner, and reaper. They reside in script/process. More details can be had by calling them with -h/--help. + +* Added load_fixtures task to the Rakefile, which will load all the fixtures into the database for the current environment #1791 [Marcel Molina] + +* Added an empty robots.txt to public/, so that web servers asking for it won't trigger a dynamic call, like favicon.ico #1738 [michael@schubert] + +* Dropped the 'immediate close-down' of FCGI processes since it didn't work consistently and produced bad responses when it didn't. So now a TERM ensures exit after the next request (just as if the process is handling a request when it receives the signal). This means that you'll have to 'nudge' all FCGI processes with a request in order to ensure that they have all reloaded. This can be done by something like ./script/process/repear --nudge 'http://www.myapp.com' --instances 10, which will load the myapp site 10 times (and thus hit all of the 10 FCGI processes once, enough to shut down). + + +*0.13.1* (11 July, 2005) + +* Look for app-specific generators in RAILS_ROOT/generators rather than the clunky old RAILS_ROOT/script/generators. Nobody really uses this feature except for the unit tests, so it's a negligible-impact change. If you want to work with third-party generators, drop them in ~/.rails/generators or simply install gems. + +* Fixed that each request with the WEBrick adapter would open a new database connection #1685 [Sam Stephenson] + +* Added support for SQL Server in the database rake tasks #1652 [ken.barker@gmail.com] Note: osql and scptxfr may need to be installed on your development environment. This involves getting the .exes and a .rll (scptxfr) from a production SQL Server (not developer level SQL Server). Add their location to your Environment PATH and you are all set. + +* Added a VERSION parameter to the migrate task that allows you to do "rake migrate VERSION=34" to migrate to the 34th version traveling up or down depending on the current version + +* Extend Ruby version check to include RUBY_RELEASE_DATE >= '2005-12-25', the final Ruby 1.8.2 release #1674 [court3nay@gmail.com] + +* Improved documentation for environment config files #1625 [court3nay@gmail.com] + + +*0.13.0* (6 July, 2005) + +* Changed the default logging level in config/environment.rb to INFO for production (so SQL statements won't be logged) + +* Added migration generator: ./script/generate migration add_system_settings + +* Added "migrate" as rake task to execute all the pending migrations from db/migrate + +* Fixed that model generator would make fixtures plural, even if ActiveRecord::Base.pluralize_table_names was false #1185 [Marcel Molina] + +* Added a DOCTYPE of HTML transitional to the HTML files generated by Rails #1124 [Michael Koziarski] + +* SIGTERM also gracefully exits dispatch.fcgi. Ignore SIGUSR1 on Windows. + +* Add the option to manually manage garbage collection in the FastCGI dispatcher. Set the number of requests between GC runs in your public/dispatch.fcgi [skaes@web.de] + +* Allow dynamic application reloading for dispatch.fcgi processes by sending a SIGHUP. If the process is currently handling a request, the request will be allowed to complete first. This allows production fcgi's to be reloaded without having to restart them. + +* RailsFCGIHandler (dispatch.fcgi) no longer tries to explicitly flush $stdout (CgiProcess#out always calls flush) + +* Fixed rakefile actions against PostgreSQL when the password is all numeric #1462 [michael@schubert.cx] + +* ActionMailer::Base subclasses are reloaded with the other rails components #1262 + +* Made the WEBrick adapter not use a mutex around action performance if ActionController::Base.allow_concurrency is true (default is false) + +* Fixed that mailer generator generated fixtures/plural while units expected fixtures/singular #1457 [Scott Barron] + +* Added a 'whiny nil' that's aim to ensure that when users pass nil to methods where that isn't appropriate, instead of NoMethodError? and the name of some method used by the framework users will see a message explaining what type of object was expected. Only active in test and development environments by default #1209 [Michael Koziarski] + +* Fixed the test_helper.rb to be safe for requiring controllers from multiple spots, like app/controllers/article_controller.rb and app/controllers/admin/article_controller.rb, without reloading the environment twice #1390 [Nicholas Seckar] + +* Fixed Webrick to escape + characters in URL's the same way that lighttpd and apache do #1397 [Nicholas Seckar] + +* Added -e/--environment option to script/runner #1408 [fbeausoleil@ftml.net] + +* Modernize the scaffold generator to use the simplified render and test methods and to change style from @params["id"] to params[:id]. #1367 + +* Added graceful exit from pressing CTRL-C during the run of the rails command #1150 [Caleb Tennis] + +* Allow graceful exits for dispatch.fcgi processes by sending a SIGUSR1. If the process is currently handling a request, the request will be allowed to complete and then will terminate itself. If a request is not being handled, the process is terminated immediately (via #exit). This basically works like restart graceful on Apache. [Jamis Buck] + +* Made dispatch.fcgi more robust by catching fluke errors and retrying unless its a permanent condition. [Jamis Buck] + +* Added console --profile for profiling an IRB session #1154 [Jeremy Kemper] + +* Changed console_sandbox into console --sandbox #1154 [Jeremy Kemper] + + +*0.12.1* (20th April, 2005) + +* Upgraded to Active Record 1.10.1, Action Pack 1.8.1, Action Mailer 0.9.1, Action Web Service 0.7.1 + + +*0.12.0* (19th April, 2005) + +* Fixed that purge_test_database would use database settings from the development environment when recreating the test database #1122 [rails@cogentdude.com] + +* Added script/benchmarker to easily benchmark one or more statement a number of times from within the environment. Examples: + + # runs the one statement 10 times + script/benchmarker 10 'Person.expensive_method(10)' + + # pits the two statements against each other with 50 runs each + script/benchmarker 50 'Person.expensive_method(10)' 'Person.cheap_method(10)' + +* Added script/profiler to easily profile a single statement from within the environment. Examples: + + script/profiler 'Person.expensive_method(10)' + script/profiler 'Person.expensive_method(10)' 10 # runs the statement 10 times + +* Added Rake target clear_logs that'll truncate all the *.log files in log/ to zero #1079 [Lucas Carlson] + +* Added lazy typing for generate, such that ./script/generate cn == ./script/generate controller and the likes #1051 [k@v2studio.com] + +* Fixed that ownership is brought over in pg_dump during tests for PostgreSQL #1060 [pburleson@gmail.com] + +* Upgraded to Active Record 1.10.0, Action Pack 1.8.0, Action Mailer 0.9.0, Action Web Service 0.7.0, Active Support 1.0.4 + + +*0.11.1* (27th March, 2005) + +* Fixed the dispatch.fcgi use of a logger + +* Upgraded to Active Record 1.9.1, Action Pack 1.7.0, Action Mailer 0.8.1, Action Web Service 0.6.2, Active Support 1.0.3 + + +*0.11.0* (22th March, 2005) + +* Removed SCRIPT_NAME from the WEBrick environment to prevent conflicts with PATH_INFO #896 [Nicholas Seckar] + +* Removed ?$1 from the dispatch.f/cgi redirect line to get rid of 'complete/path/from/request.html' => nil being in the @params now that the ENV["REQUEST_URI"] is used to determine the path #895 [dblack/Nicholas Seckar] + +* Added additional error handling to the FastCGI dispatcher to catch even errors taking down the entire process + +* Improved the generated scaffold code a lot to take advantage of recent Rails developments #882 [Tobias Luetke] + +* Combined the script/environment.rb used for gems and regular files version. If vendor/rails/* has all the frameworks, then files version is used, otherwise gems #878 [Nicholas Seckar] + +* Changed .htaccess to allow dispatch.* to be called from a sub-directory as part of the push with Action Pack to make Rails work on non-vhost setups #826 [Nicholas Seckar/Tobias Luetke] + +* Added script/runner which can be used to run code inside the environment by eval'ing the first parameter. Examples: + + ./script/runner 'ReminderService.deliver' + ./script/runner 'Mailer.receive(STDIN.read)' + + This makes it easier to do CRON and postfix scripts without actually making a script just to trigger 1 line of code. + +* Fixed webrick_server cookie handling to allow multiple cookes to be set at once #800, #813 [dave@cherryville.org] + +* Fixed the Rakefile's interaction with postgresql to: + + 1. Use PGPASSWORD and PGHOST in the environment to fix prompting for + passwords when connecting to a remote db and local socket connections. + 2. Add a '-x' flag to pg_dump which stops it dumping privileges #807 [rasputnik] + 3. Quote the user name and use template0 when dumping so the functions doesn't get dumped too #855 [pburleson] + 4. Use the port if available #875 [madrobby] + +* Upgraded to Active Record 1.9.0, Action Pack 1.6.0, Action Mailer 0.8.0, Action Web Service 0.6.1, Active Support 1.0.2 + + +*0.10.1* (7th March, 2005) + +* Fixed rake stats to ignore editor backup files like model.rb~ #791 [skanthak] + +* Added exception shallowing if the DRb server can't be started (not worth making a fuss about to distract new users) #779 [Tobias Luetke] + +* Added an empty favicon.ico file to the public directory of new applications (so the logs are not spammed by its absence) + +* Fixed that scaffold generator new template should use local variable instead of instance variable #778 [Dan Peterson] + +* Allow unit tests to run on a remote server for PostgreSQL #781 [adamm@galacticasoftware.com] + +* Added web_service generator (run ./script/generate web_service for help) #776 [Leon Bredt] + +* Added app/apis and components to code statistics report #729 [Scott Barron] + +* Fixed WEBrick server to use ABSOLUTE_RAILS_ROOT instead of working_directory #687 [Nicholas Seckar] + +* Fixed rails_generator to be usable without RubyGems #686 [Cristi BALAN] + +* Fixed -h/--help for generate and destroy generators #331 + +* Added begin/rescue around the FCGI dispatcher so no uncaught exceptions can bubble up to kill the process (logs to log/fastcgi.crash.log) + +* Fixed that association#count would produce invalid sql when called sequentialy #659 [kanis@comcard.de] + +* Fixed test/mocks/testing to the correct test/mocks/test #740 + +* Added early failure if the Ruby version isn't 1.8.2 or above #735 + +* Removed the obsolete -i/--index option from the WEBrick servlet #743 + +* Upgraded to Active Record 1.8.0, Action Pack 1.5.1, Action Mailer 0.7.1, Action Web Service 0.6.0, Active Support 1.0.1 + + +*0.10.0* (24th February, 2005) + +* Changed default IP binding for WEBrick from 127.0.0.1 to 0.0.0.0 so that the server is accessible both locally and remotely #696 [Marcel] + +* Fixed that script/server -d was broken so daemon mode couldn't be used #687 [Nicholas Seckar] + +* Upgraded to breakpoint 92 which fixes: + + * overload IRB.parse_opts(), fixes #443 + => breakpoints in tests work even when running them via rake + * untaint handlers, might fix an issue discussed on the Rails ML + * added verbose mode to breakpoint_client + * less noise caused by breakpoint_client by default + * ignored TerminateLineInput exception in signal handler + => quiet exit on Ctrl-C + +* Added support for independent components residing in /components. Example: + + Controller: components/list/items_controller.rb + (holds a List::ItemsController class with uses_component_template_root called) + + Model : components/list/item.rb + (namespace is still shared, so an Item model in app/models will take precedence) + + Views : components/list/items/show.rhtml + + +* Added --sandbox option to script/console that'll roll back all changes made to the database when you quit #672 [Jeremy Kemper] + +* Added 'recent' as a rake target that'll run tests for files that changed in the last 10 minutes #612 [Jeremy Kemper] + +* Changed script/console to default to development environment and drop --no-inspect #650 [Jeremy Kemper] + +* Added that the 'fixture :posts' syntax can be used for has_and_belongs_to_many fixtures where a model doesn't exist #572 [Jeremy Kemper] + +* Added that running test_units and test_functional now performs the clone_structure_to_test as well #566 [rasputnik] + +* Added new generator framework that informs about its doings on generation and enables updating and destruction of generated artifacts. See the new script/destroy and script/update for more details #487 [Jeremy Kemper] + +* Added Action Web Service as a new add-on framework for Action Pack [Leon Bredt] + +* Added Active Support as an independent utility and standard library extension bundle + +* Upgraded to Active Record 1.7.0, Action Pack 1.5.0, Action Mailer 0.7.0 + + +*0.9.5* (January 25th, 2005) + +* Fixed dependency reloading by switching to a remove_const approach where all Active Records, Active Record Observers, and Action Controllers are reloading by undefining their classes. This enables you to remove methods in all three types and see the change reflected immediately and it fixes #539. This also means that only those three types of classes will benefit from the const_missing and reloading approach. If you want other classes (like some in lib/) to reload, you must use require_dependency to do it. + +* Added Florian Gross' latest version of Breakpointer and friends that fixes a variaty of bugs #441 [Florian Gross] + +* Fixed skeleton Rakefile to work with sqlite3 out of the box #521 [rasputnik] + +* Fixed that script/breakpointer didn't get the Ruby path rewritten as the other scripts #523 [brandt@kurowski.net] + +* Fixed handling of syntax errors in models that had already been succesfully required once in the current interpreter + +* Fixed that models that weren't referenced in associations weren't being reloaded in the development mode by reinstating the reload + +* Fixed that generate scaffold would produce bad functional tests + +* Fixed that FCGI can also display SyntaxErrors + +* Upgraded to Active Record 1.6.0, Action Pack 1.4.0 + + +*0.9.4.1* (January 18th, 2005) + +* Added 5-second timeout to WordNet alternatives on creating reserved-word models #501 [Marcel Molina] + +* Fixed binding of caller #496 [Alexey] + +* Upgraded to Active Record 1.5.1, Action Pack 1.3.1, Action Mailer 0.6.1 + + +*0.9.4* (January 17th, 2005) + +* Added that ApplicationController will catch a ControllerNotFound exception if someone attempts to access a url pointing to an unexisting controller [Tobias Luetke] + +* Flipped code-to-test ratio around to be more readable #468 [Scott Baron] + +* Fixed log file permissions to be 666 instead of 777 (so they're not executable) #471 [Lucas Carlson] + +* Fixed that auto reloading would some times not work or would reload the models twice #475 [Tobias Luetke] + +* Added rewrite rules to deal with caching to public/.htaccess + +* Added the option to specify a controller name to "generate scaffold" and made the default controller name the plural form of the model. + +* Added that rake clone_structure_to_test, db_structure_dump, and purge_test_database tasks now pick up the source database to use from + RAILS_ENV instead of just forcing development #424 [Tobias Luetke] + +* Fixed script/console to work with Windows (that requires the use of irb.bat) #418 [octopod] + +* Fixed WEBrick servlet slowdown over time by restricting the load path reloading to mod_ruby + +* Removed Fancy Indexing as a default option on the WEBrick servlet as it made it harder to use various caching schemes + +* Upgraded to Active Record 1.5, Action Pack 1.3, Action Mailer 0.6 + + +*0.9.3* (January 4th, 2005) + +* Added support for SQLite in the auto-dumping/importing of schemas for development -> test #416 + +* Added automated rewriting of the shebang lines on installs through the gem rails command #379 [Manfred Stienstra] + +* Added ActionMailer::Base.deliver_method = :test to the test environment so that mail objects are available in ActionMailer::Base.deliveries + for functional testing. + +* Added protection for creating a model through the generators with a name of an existing class, like Thread or Date. + It'll even offer you a synonym using wordnet.princeton.edu as a look-up. No, I'm not kidding :) [Florian Gross] + +* Fixed dependency management to happen in a unified fashion for Active Record and Action Pack using the new Dependencies module. This means that + the environment options needs to change from: + + Before in development.rb: + ActionController::Base.reload_dependencies = true   + ActiveRecord::Base.reload_associations     = true + + Now in development.rb: + Dependencies.mechanism = :load + + Before in production.rb and test.rb: + ActionController::Base.reload_dependencies = false + ActiveRecord::Base.reload_associations     = false + + Now in production.rb and test.rb: + Dependencies.mechanism = :require + +* Fixed problems with dependency caching and controller hierarchies on Ruby 1.8.2 in development mode #351 + +* Fixed that generated action_mailers doesnt need to require the action_mailer since thats already done in the environment #382 [Lucas Carlson] + +* Upgraded to Action Pack 1.2.0 and Active Record 1.4.0 + + +*0.9.2* + +* Fixed CTRL-C exists from the Breakpointer to be a clean affair without error dumping [Kent Sibilev] + +* Fixed "rake stats" to work with sub-directories in models and controllers and to report the code to test ration [Scott Baron] + +* Added that Active Record associations are now reloaded instead of cleared to work with the new const_missing hook in Active Record. + +* Added graceful handling of an inaccessible log file by redirecting output to STDERR with a warning #330 [rainmkr] + +* Added support for a -h/--help parameter in the generator #331 [Ulysses] + +* Fixed that File.expand_path in config/environment.rb would fail when dealing with symlinked public directories [mjobin] + +* Upgraded to Action Pack 1.1.0 and Active Record 1.3.0 + + +*0.9.1* + +* Upgraded to Action Pack 1.0.1 for important bug fix + +* Updated gem dependencies + + +*0.9.0* + +* Renamed public/dispatch.servlet to script/server -- it wasn't really dispatching anyway as its delegating calls to public/dispatch.rb + +* Renamed AbstractApplicationController and abstract_application.rb to ApplicationController and application.rb, so that it will be possible + for the framework to automatically pick up on app/views/layouts/application.rhtml and app/helpers/application.rb + +* Added script/console that makes it even easier to start an IRB session for interacting with the domain model. Run with no-args to + see help. + +* Added breakpoint support through the script/breakpointer client. This means that you can break out of execution at any point in + the code, investigate and change the model, AND then resume execution! Example: + + class WeblogController < ActionController::Base + def index + @posts = Post.find_all + breakpoint "Breaking out from the list" + end + end + + So the controller will accept the action, run the first line, then present you with a IRB prompt in the breakpointer window. + Here you can do things like: + + Executing breakpoint "Breaking out from the list" at .../webrick_server.rb:16 in 'breakpoint' + + >> @posts.inspect + => "[#nil, \"body\"=>nil, \"id\"=>\"1\"}>, + #\"Rails you know!\", \"body\"=>\"Only ten..\", \"id\"=>\"2\"}>]" + >> @posts.first.title = "hello from a breakpoint" + => "hello from a breakpoint" + + ...and even better is that you can examine how your runtime objects actually work: + + >> f = @posts.first + => #nil, "body"=>nil, "id"=>"1"}> + >> f. + Display all 152 possibilities? (y or n) + + Finally, when you're ready to resume execution, you press CTRL-D + +* Changed environments to be configurable through an environment variable. By default, the environment is "development", but you + can change that and set your own by configuring the Apache vhost with a string like (mod_env must be available on the server): + + SetEnv RAILS_ENV production + + ...if you're using WEBrick, you can pick the environment to use with the command-line parameters -e/--environment, like this: + + ruby public/dispatcher.servlet -e production + +* Added a new default environment called "development", which leaves the production environment to be tuned exclusively for that. + +* Added a start_server in the root of the Rails application to make it even easier to get started + +* Fixed public/.htaccess to use RewriteBase and share the same rewrite rules for all the dispatch methods + +* Fixed webrick_server to handle requests in a serialized manner (the Rails reloading infrastructure is not thread-safe) + +* Added support for controllers in directories. So you can have: + + app/controllers/account_controller.rb # URL: /account/ + app/controllers/admin/account_controller.rb # URL: /admin/account/ + + NOTE: You need to update your public/.htaccess with the new rules to pick it up + +* Added reloading for associations and dependencies under cached environments like FastCGI and mod_ruby. This makes it possible to use + those environments for development. This is turned on by default, but can be turned off with + ActiveRecord::Base.reload_associations = false and ActionController::Base.reload_dependencies = false in production environments. + +* Added support for sub-directories in app/models. So now you can have something like Basecamp with: + + app/models/accounting + app/models/project + app/models/participants + app/models/settings + + It's poor man's namespacing, but only for file-system organization. You still require files just like before. + Nothing changes inside the files themselves. + + +* Fixed a few references in the tests generated by new_mailer [Jeremy Kemper] + +* Added support for mocks in testing with test/mocks + +* Cleaned up the environments a bit and added global constant RAILS_ROOT + + +*0.8.5* (9) + +* Made dev-util available to all tests, so you can insert breakpoints in any test case to get an IRB prompt at that point [Jeremy Kemper]: + + def test_complex_stuff + @david.projects << @new_project + breakpoint "Let's have a closer look at @david" + end + + You need to install dev-utils yourself for this to work ("gem install dev-util"). + +* Added shared generator behavior so future upgrades should be possible without manually copying over files [Jeremy Kemper] + +* Added the new helper style to both controller and helper templates [Jeremy Kemper] + +* Added new_crud generator for creating a model and controller at the same time with explicit scaffolding [Jeremy Kemper] + +* Added configuration of Test::Unit::TestCase.fixture_path to test_helper to concide with the new AR fixtures style + +* Fixed that new_model was generating singular table/fixture names + +* Upgraded to Action Mailer 0.4.0 + +* Upgraded to Action Pack 0.9.5 + +* Upgraded to Active Record 1.1.0 + + +*0.8.0 (15)* + +* Removed custom_table_name option for new_model now that the Inflector is as powerful as it is + +* Changed the default rake action to just do testing and separate API generation and coding statistics into a "doc" task. + +* Fixed WEBrick dispatcher to handle missing slashes in the URLs gracefully [alexey] + +* Added user option for all postgresql tool calls in the rakefile [elvstone] + +* Fixed problem with running "ruby public/dispatch.servlet" instead of "cd public; ruby dispatch.servlet" [alexey] + +* Fixed WEBrick server so that it no longer hardcodes the ruby interpreter used to "ruby" but will get the one used based + on the Ruby runtime configuration. [Marcel Molina Jr.] + +* Fixed Dispatcher so it'll route requests to magic_beans to MagicBeansController/magic_beans_controller.rb [Caio Chassot] + +* "new_controller MagicBeans" and "new_model SubscriptionPayments" will now both behave properly as they use the new Inflector. + +* Fixed problem with MySQL foreign key constraint checks in Rake :clone_production_structure_to_test target [Andreas Schwarz] + +* Changed WEBrick server to by default be auto-reloading, which is slower but makes source changes instant. + Class compilation cache can be turned on with "-c" or "--cache-classes". + +* Added "-b/--binding" option to WEBrick dispatcher to bind the server to a specific IP address (default: 127.0.0.1) [Kevin Temp] + +* dispatch.fcgi now DOESN'T set FCGI_PURE_RUBY as it was slowing things down for now reason [Andreas Schwarz] + +* Added new_mailer generator to work with Action Mailer + +* Included new framework: Action Mailer 0.3 + +* Upgraded to Action Pack 0.9.0 + +* Upgraded to Active Record 1.0.0 + + +*0.7.0* + +* Added an optional second argument to the new_model script that allows the programmer to specify the table name, + which will used to generate a custom table_name method in the model and will also be used in the creation of fixtures. + [Kevin Radloff] + +* script/new_model now turns AccountHolder into account_holder instead of accountholder [Kevin Radloff] + +* Fixed the faulty handleing of static files with WEBrick [Andreas Schwarz] + +* Unified function_test_helper and unit_test_helper into test_helper + +* Fixed bug with the automated production => test database dropping on PostgreSQL [dhawkins] + +* create_fixtures in both the functional and unit test helper now turns off the log during fixture generation + and can generate more than one fixture at a time. Which makes it possible for assignments like: + + @people, @projects, @project_access, @companies, @accounts = + create_fixtures "people", "projects", "project_access", "companies", "accounts" + +* Upgraded to Action Pack 0.8.5 (locally-scoped variables, partials, advanced send_file) + +* Upgraded to Active Record 0.9.5 (better table_name guessing, cloning, find_all_in_collection) + + +*0.6.5* + +* No longer specifies a template for rdoc, so it'll use whatever is default (you can change it in the rakefile) + +* The new_model generator will now use the same rules for plural wordings as Active Record + (so Category will give categories, not categorys) [Kevin Radloff] + +* dispatch.fcgi now sets FCGI_PURE_RUBY to true to ensure that it's the Ruby version that's loaded [danp] + +* Made the GEM work with Windows + +* Fixed bug where mod_ruby would "forget" the load paths added when switching between controllers + +* PostgreSQL are now supported for the automated production => test database dropping [Kevin Radloff] + +* Errors thrown by the dispatcher are now properly handled in FCGI. + +* Upgraded to Action Pack 0.8.0 (lots and lots and lots of fixes) + +* Upgraded to Active Record 0.9.4 (a bunch of fixes) + + +*0.6.0* + +* Added AbstractionApplicationController as a superclass for all controllers generated. This class can be used + to carry filters and methods that are to be shared by all. It has an accompanying ApplicationHelper that all + controllers will also automatically have available. + +* Added environments that can be included from any script to get the full Active Record and Action Controller + context running. This can be used by maintenance scripts or to interact with the model through IRB. Example: + + require 'config/environments/production' + + for account in Account.find_all + account.recalculate_interests + end + + A short migration script for an account model that had it's interest calculation strategy changed. + +* Accessing the index of a controller with "/weblog" will now redirect to "/weblog/" (only on Apache, not WEBrick) + +* Simplified the default Apache config so even remote requests are served off CGI as a default. + You'll now have to do something specific to activate mod_ruby and FCGI (like using the force urls). + This should make it easier for new comers that start on an external server. + +* Added more of the necessary Apache options to .htaccess to make it easier to setup + +* Upgraded to Action Pack 0.7.9 (lots of fixes) + +* Upgraded to Active Record 0.9.3 (lots of fixes) + + +*0.5.7* + +* Fixed bug in the WEBrick dispatcher that prevented it from getting parameters from the URL + (through GET requests or otherwise) + +* Added lib in root as a place to store app specific libraries + +* Added lib and vendor to load_path, so anything store within can be loaded directly. + Hence lib/redcloth.rb can be loaded with require "redcloth" + +* Upgraded to Action Pack 0.7.8 (lots of fixes) + +* Upgraded to Active Record 0.9.2 (minor upgrade) + + +*0.5.6* + +* Upgraded to Action Pack 0.7.7 (multipart form fix) + +* Updated the generated template stubs to valid XHTML files + +* Ensure that controllers generated are capitalized, so "new_controller TodoLists" + gives the same as "new_controller Todolists" and "new_controller todolists". + + +*0.5.5* + +* Works on Windows out of the box! (Dropped symlinks) + +* Added webrick dispatcher: Try "ruby public/dispatch.servlet --help" [Florian Gross] + +* Report errors about initialization to browser (instead of attempting to use uninitialized logger) + +* Upgraded to Action Pack 0.7.6 + +* Upgraded to Active Record 0.9.1 + +* Added distinct 500.html instead of reusing 404.html + +* Added MIT license + + +*0.5.0* + +* First public release diff --git a/vendor/rails/railties/MIT-LICENSE b/vendor/rails/railties/MIT-LICENSE new file mode 100644 index 00000000..5919c288 --- /dev/null +++ b/vendor/rails/railties/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2004 David Heinemeier Hansson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/rails/railties/README b/vendor/rails/railties/README new file mode 100644 index 00000000..7d8965e6 --- /dev/null +++ b/vendor/rails/railties/README @@ -0,0 +1,183 @@ +== Welcome to Rails + +Rails is a web-application and persistence framework that includes everything +needed to create database-backed web-applications according to the +Model-View-Control pattern of separation. This pattern splits the view (also +called the presentation) into "dumb" templates that are primarily responsible +for inserting pre-built data in between HTML tags. The model contains the +"smart" domain objects (such as Account, Product, Person, Post) that holds all +the business logic and knows how to persist themselves to a database. The +controller handles the incoming requests (such as Save New Account, Update +Product, Show Post) by manipulating the model and directing data to the view. + +In Rails, the model is handled by what's called an object-relational mapping +layer entitled Active Record. This layer allows you to present the data from +database rows as objects and embellish these data objects with business logic +methods. You can read more about Active Record in +link:files/vendor/rails/activerecord/README.html. + +The controller and view are handled by the Action Pack, which handles both +layers by its two parts: Action View and Action Controller. These two layers +are bundled in a single package due to their heavy interdependence. This is +unlike the relationship between the Active Record and Action Pack that is much +more separate. Each of these packages can be used independently outside of +Rails. You can read more about Action Pack in +link:files/vendor/rails/actionpack/README.html. + + +== Getting started + +1. Start the web server: ruby script/server (run with --help for options) +2. Go to http://localhost:3000/ and get "Welcome aboard: You’re riding the Rails!" +3. Follow the guidelines to start developing your application + + +== Web servers + +Rails uses the built-in web server in Ruby called WEBrick by default, so you don't +have to install or configure anything to play around. + +If you have lighttpd installed, though, it'll be used instead when running script/server. +It's considerably faster than WEBrick and suited for production use, but requires additional +installation and currently only works well on OS X/Unix (Windows users are encouraged +to start with WEBrick). We recommend version 1.4.11 and higher. You can download it from +http://www.lighttpd.net. + +If you want something that's halfway between WEBrick and lighttpd, we heartily recommend +Mongrel. It's a Ruby-based web server with a C-component (so it requires compilation) that +also works very well with Windows. See more at http://mongrel.rubyforge.org/. + +But of course its also possible to run Rails with the premiere open source web server Apache. +To get decent performance, though, you'll need to install FastCGI. For Apache 1.3, you want +to use mod_fastcgi. For Apache 2.0+, you want to use mod_fcgid. + +See http://wiki.rubyonrails.com/rails/pages/FastCGI for more information on FastCGI. + +== Example for Apache conf + + + ServerName rails + DocumentRoot /path/application/public/ + ErrorLog /path/application/log/server.log + + + Options ExecCGI FollowSymLinks + AllowOverride all + Allow from all + Order allow,deny + + + +NOTE: Be sure that CGIs can be executed in that directory as well. So ExecCGI +should be on and ".cgi" should respond. All requests from 127.0.0.1 go +through CGI, so no Apache restart is necessary for changes. All other requests +go through FCGI (or mod_ruby), which requires a restart to show changes. + + +== Debugging Rails + +Have "tail -f" commands running on both the server.log, production.log, and +test.log files. Rails will automatically display debugging and runtime +information to these files. Debugging info will also be shown in the browser +on requests from 127.0.0.1. + + +== Breakpoints + +Breakpoint support is available through the script/breakpointer client. This +means that you can break out of execution at any point in the code, investigate +and change the model, AND then resume execution! Example: + + class WeblogController < ActionController::Base + def index + @posts = Post.find_all + breakpoint "Breaking out from the list" + end + end + +So the controller will accept the action, run the first line, then present you +with a IRB prompt in the breakpointer window. Here you can do things like: + +Executing breakpoint "Breaking out from the list" at .../webrick_server.rb:16 in 'breakpoint' + + >> @posts.inspect + => "[#nil, \"body\"=>nil, \"id\"=>\"1\"}>, + #\"Rails you know!\", \"body\"=>\"Only ten..\", \"id\"=>\"2\"}>]" + >> @posts.first.title = "hello from a breakpoint" + => "hello from a breakpoint" + +...and even better is that you can examine how your runtime objects actually work: + + >> f = @posts.first + => #nil, "body"=>nil, "id"=>"1"}> + >> f. + Display all 152 possibilities? (y or n) + +Finally, when you're ready to resume execution, you press CTRL-D + + +== Console + +You can interact with the domain model by starting the console through script/console. +Here you'll have all parts of the application configured, just like it is when the +application is running. You can inspect domain models, change values, and save to the +database. Starting the script without arguments will launch it in the development environment. +Passing an argument will specify a different environment, like script/console production. + +To reload your controllers and models after launching the console run reload! + + + +== Description of contents + +app + Holds all the code that's specific to this particular application. + +app/controllers + Holds controllers that should be named like weblog_controller.rb for + automated URL mapping. All controllers should descend from + ActionController::Base. + +app/models + Holds models that should be named like post.rb. + Most models will descend from ActiveRecord::Base. + +app/views + Holds the template files for the view that should be named like + weblog/index.rhtml for the WeblogController#index action. All views use eRuby + syntax. This directory can also be used to keep stylesheets, images, and so on + that can be symlinked to public. + +app/helpers + Holds view helpers that should be named like weblog_helper.rb. + +app/apis + Holds API classes for web services. + +config + Configuration files for the Rails environment, the routing map, the database, and other dependencies. + +components + Self-contained mini-applications that can bundle together controllers, models, and views. + +db + Contains the database schema in schema.rb. db/migrate contains all + the sequence of Migrations for your schema. + +lib + Application specific libraries. Basically, any kind of custom code that doesn't + belong under controllers, models, or helpers. This directory is in the load path. + +public + The directory available for the web server. Contains subdirectories for images, stylesheets, + and javascripts. Also contains the dispatchers and the default HTML files. + +script + Helper scripts for automation and generation. + +test + Unit and functional tests along with fixtures. + +vendor + External libraries that the application depends on. Also includes the plugins subdirectory. + This directory is in the load path. diff --git a/vendor/rails/railties/Rakefile b/vendor/rails/railties/Rakefile new file mode 100644 index 00000000..3560fafc --- /dev/null +++ b/vendor/rails/railties/Rakefile @@ -0,0 +1,320 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' +require 'rake/gempackagetask' +require 'rake/contrib/rubyforgepublisher' + +require 'date' +require 'rbconfig' + +require File.join(File.dirname(__FILE__), 'lib/rails', 'version') + +PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' +PKG_NAME = 'rails' +PKG_VERSION = Rails::VERSION::STRING + PKG_BUILD +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" +PKG_DESTINATION = ENV["RAILS_PKG_DESTINATION"] || "../#{PKG_NAME}" + +RELEASE_NAME = "REL #{PKG_VERSION}" + +RUBY_FORGE_PROJECT = "rails" +RUBY_FORGE_USER = "webster132" + + +# Rake::TestTask.new("test") do |t| +# t.libs << 'test' +# t.pattern = 'test/*_test.rb' +# t.verbose = true +# end + + +BASE_DIRS = %w( + app config/environments components db doc log lib lib/tasks public script script/performance script/process test vendor vendor/plugins + tmp/sessions tmp/cache tmp/sockets +) + +APP_DIRS = %w( models controllers helpers views views/layouts ) +PUBLIC_DIRS = %w( images javascripts stylesheets ) +TEST_DIRS = %w( fixtures unit functional mocks mocks/development mocks/test ) + +LOG_FILES = %w( server.log development.log test.log production.log ) +HTML_FILES = %w( 404.html 500.html index.html robots.txt favicon.ico images/rails.png + javascripts/prototype.js javascripts/application.js + javascripts/effects.js javascripts/dragdrop.js javascripts/controls.js ) +BIN_FILES = %w( about breakpointer console destroy generate performance/benchmarker performance/profiler process/reaper process/spawner runner server plugin ) + +VENDOR_LIBS = %w( actionpack activerecord actionmailer activesupport actionwebservice railties ) + + +desc "Generates a fresh Rails package with documentation" +task :fresh_rails => [ :clean, :make_dir_structure, :initialize_file_stubs, :copy_vendor_libraries, :copy_ties_content, :generate_documentation ] + +desc "Generates a fresh Rails package using GEMs with documentation" +task :fresh_gem_rails => [ :clean, :make_dir_structure, :initialize_file_stubs, :copy_ties_content, :copy_gem_environment ] + +desc "Generates a fresh Rails package without documentation (faster)" +task :fresh_rails_without_docs => [ :clean, :make_dir_structure, :initialize_file_stubs, :copy_vendor_libraries, :copy_ties_content ] + +desc "Generates a fresh Rails package without documentation (faster)" +task :fresh_rails_without_docs_using_links => [ :clean, :make_dir_structure, :initialize_file_stubs, :link_vendor_libraries, :copy_ties_content ] + +desc "Generates minimal Rails package using symlinks" +task :dev => [ :clean, :make_dir_structure, :initialize_file_stubs, :link_vendor_libraries, :copy_ties_content ] + +desc "Packages the fresh Rails package with documentation" +task :package => [ :clean, :fresh_rails ] do + system %{cd ..; tar -czvf #{PKG_NAME}-#{PKG_VERSION}.tgz #{PKG_NAME}} + system %{cd ..; zip -r #{PKG_NAME}-#{PKG_VERSION}.zip #{PKG_NAME}} +end + +task :clean do + rm_rf PKG_DESTINATION +end + +# Get external spinoffs ------------------------------------------------------------------- + +desc "Updates railties to the latest version of the javascript spinoffs" +task :update_js do + for js in %w( prototype controls dragdrop effects ) + rm "html/javascripts/#{js}.js" + cp "./../actionpack/lib/action_view/helpers/javascripts/#{js}.js", "html/javascripts" + end +end + +# Make directory structure ---------------------------------------------------------------- + +def make_dest_dirs(dirs, path = nil) + mkdir_p dirs.map { |dir| File.join(PKG_DESTINATION, path.to_s, dir) } +end + +desc "Make the directory structure for the new Rails application" +task :make_dir_structure => [ :make_base_dirs, :make_app_dirs, :make_public_dirs, :make_test_dirs ] + +task(:make_base_dirs) { make_dest_dirs BASE_DIRS } +task(:make_app_dirs) { make_dest_dirs APP_DIRS, 'app' } +task(:make_public_dirs) { make_dest_dirs PUBLIC_DIRS, 'public' } +task(:make_test_dirs) { make_dest_dirs TEST_DIRS, 'test' } + + +# Initialize file stubs ------------------------------------------------------------------- + +desc "Initialize empty file stubs (such as for logging)" +task :initialize_file_stubs => [ :initialize_log_files ] + +task :initialize_log_files do + log_dir = File.join(PKG_DESTINATION, 'log') + chmod 0777, log_dir + LOG_FILES.each do |log_file| + log_path = File.join(log_dir, log_file) + touch log_path + chmod 0666, log_path + end +end + + +# Copy Vendors ---------------------------------------------------------------------------- + +desc "Copy in all the Rails packages to vendor" +task :copy_vendor_libraries do + mkdir File.join(PKG_DESTINATION, 'vendor', 'rails') + VENDOR_LIBS.each { |dir| cp_r File.join('..', dir), File.join(PKG_DESTINATION, 'vendor', 'rails', dir) } + FileUtils.rm_r(Dir.glob(File.join(PKG_DESTINATION, 'vendor', 'rails', "**", ".svn"))) +end + +desc "Link in all the Rails packages to vendor" +task :link_vendor_libraries do + mkdir File.join(PKG_DESTINATION, 'vendor', 'rails') + VENDOR_LIBS.each { |dir| ln_s File.join('..', '..', '..', dir), File.join(PKG_DESTINATION, 'vendor', 'rails', dir) } +end + + +# Copy Ties Content ----------------------------------------------------------------------- + +# :link_apache_config +desc "Make copies of all the default content of ties" +task :copy_ties_content => [ + :copy_rootfiles, :copy_dispatches, :copy_html_files, :copy_application, + :copy_configs, :copy_binfiles, :copy_test_helpers, :copy_app_doc_readme ] + +task :copy_dispatches do + copy_with_rewritten_ruby_path("dispatches/dispatch.rb", "#{PKG_DESTINATION}/public/dispatch.rb") + chmod 0755, "#{PKG_DESTINATION}/public/dispatch.rb" + + copy_with_rewritten_ruby_path("dispatches/dispatch.rb", "#{PKG_DESTINATION}/public/dispatch.cgi") + chmod 0755, "#{PKG_DESTINATION}/public/dispatch.cgi" + + copy_with_rewritten_ruby_path("dispatches/dispatch.fcgi", "#{PKG_DESTINATION}/public/dispatch.fcgi") + chmod 0755, "#{PKG_DESTINATION}/public/dispatch.fcgi" + + # copy_with_rewritten_ruby_path("dispatches/gateway.cgi", "#{PKG_DESTINATION}/public/gateway.cgi") + # chmod 0755, "#{PKG_DESTINATION}/public/gateway.cgi" +end + +task :copy_html_files do + HTML_FILES.each { |file| cp File.join('html', file), File.join(PKG_DESTINATION, 'public', file) } +end + +task :copy_application do + cp "helpers/application.rb", "#{PKG_DESTINATION}/app/controllers/application.rb" + cp "helpers/application_helper.rb", "#{PKG_DESTINATION}/app/helpers/application_helper.rb" +end + +task :copy_configs do + app_name = "rails" + socket = nil + require 'erb' + File.open("#{PKG_DESTINATION}/config/database.yml", 'w') {|f| f.write ERB.new(IO.read("configs/databases/mysql.yml"), nil, '-').result(binding)} + + cp "configs/routes.rb", "#{PKG_DESTINATION}/config/routes.rb" + + cp "configs/apache.conf", "#{PKG_DESTINATION}/public/.htaccess" + + cp "environments/boot.rb", "#{PKG_DESTINATION}/config/boot.rb" + cp "environments/environment.rb", "#{PKG_DESTINATION}/config/environment.rb" + cp "environments/production.rb", "#{PKG_DESTINATION}/config/environments/production.rb" + cp "environments/development.rb", "#{PKG_DESTINATION}/config/environments/development.rb" + cp "environments/test.rb", "#{PKG_DESTINATION}/config/environments/test.rb" +end + +task :copy_binfiles do + BIN_FILES.each do |file| + dest_file = File.join(PKG_DESTINATION, 'script', file) + copy_with_rewritten_ruby_path(File.join('bin', file), dest_file) + chmod 0755, dest_file + end +end + +task :copy_rootfiles do + cp "fresh_rakefile", "#{PKG_DESTINATION}/Rakefile" + cp "README", "#{PKG_DESTINATION}/README" + cp "CHANGELOG", "#{PKG_DESTINATION}/CHANGELOG" +end + +task :copy_test_helpers do + cp "helpers/test_helper.rb", "#{PKG_DESTINATION}/test/test_helper.rb" +end + +task :copy_app_doc_readme do + cp "doc/README_FOR_APP", "#{PKG_DESTINATION}/doc/README_FOR_APP" +end + +task :link_apache_config do + chdir(File.join(PKG_DESTINATION, 'config')) { + ln_s "../public/.htaccess", "apache.conf" + } +end + +def copy_with_rewritten_ruby_path(src_file, dest_file) + ruby = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']) + + File.open(dest_file, 'w') do |df| + File.open(src_file) do |sf| + line = sf.gets + if (line =~ /#!.+ruby\s*/) != nil + df.puts("#!#{ruby}") + else + df.puts(line) + end + df.write(sf.read) + end + end +end + + +# Generate documentation ------------------------------------------------------------------ + +desc "Generate documentation for the framework and for the empty application" +task :generate_documentation => [ :generate_app_doc, :generate_rails_framework_doc ] + +task :generate_rails_framework_doc do + system %{cd #{PKG_DESTINATION}; rake apidoc} +end + +task :generate_app_doc do + File.cp "doc/README_FOR_APP", "#{PKG_DESTINATION}/doc/README_FOR_APP" + system %{cd #{PKG_DESTINATION}; rake appdoc} +end + +Rake::RDocTask.new { |rdoc| + rdoc.rdoc_dir = 'doc' + rdoc.title = "Railties -- Gluing the Engine to the Rails" + rdoc.options << '--line-numbers' << '--inline-source' << '--accessor' << 'cattr_accessor=object' + rdoc.template = "#{ENV['template']}.rb" if ENV['template'] + rdoc.rdoc_files.include('README', 'CHANGELOG') + rdoc.rdoc_files.include('lib/*.rb') + rdoc.rdoc_files.include('lib/rails_generator/*.rb') + rdoc.rdoc_files.include('lib/commands/**/*.rb') +} + +# Generate GEM ---------------------------------------------------------------------------- + +task :copy_gem_environment do + cp "environments/environment.rb", "#{PKG_DESTINATION}/config/environment.rb" + chmod 0755, dest_file +end + + +PKG_FILES = FileList[ + '[a-zA-Z]*', + 'bin/**/*', + 'builtin/**/*', + 'configs/**/*', + 'doc/**/*', + 'dispatches/**/*', + 'environments/**/*', + 'helpers/**/*', + 'generators/**/*', + 'html/**/*', + 'lib/**/*' +] + +spec = Gem::Specification.new do |s| + s.name = 'rails' + s.version = PKG_VERSION + s.summary = "Web-application framework with template engine, control-flow layer, and ORM." + s.description = <<-EOF + Rails is a framework for building web-application using CGI, FCGI, mod_ruby, or WEBrick + on top of either MySQL, PostgreSQL, SQLite, DB2, SQL Server, or Oracle with eRuby- or Builder-based templates. + EOF + + s.add_dependency('rake', '>= 0.7.1') + s.add_dependency('activesupport', '= 1.3.1' + PKG_BUILD) + s.add_dependency('activerecord', '= 1.14.4' + PKG_BUILD) + s.add_dependency('actionpack', '= 1.12.5' + PKG_BUILD) + s.add_dependency('actionmailer', '= 1.2.5' + PKG_BUILD) + s.add_dependency('actionwebservice', '= 1.1.6' + PKG_BUILD) + + s.rdoc_options << '--exclude' << '.' + s.has_rdoc = false + + s.files = PKG_FILES.to_a.delete_if {|f| f.include?('.svn')} + s.require_path = 'lib' + + s.bindir = "bin" # Use these for applications. + s.executables = ["rails"] + s.default_executable = "rails" + + s.author = "David Heinemeier Hansson" + s.email = "david@loudthinking.com" + s.homepage = "http://www.rubyonrails.org" + s.rubyforge_project = "rails" +end + +Rake::GemPackageTask.new(spec) do |pkg| +end + + +# Publishing ------------------------------------------------------- +desc "Publish the API documentation" +task :pgem => [:gem] do + Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload + `ssh davidhh@wrath.rubyonrails.org './gemupdate.sh'` +end + +desc "Publish the release files to RubyForge." +task :release => [ :gem ] do + `rubyforge login` + release_command = "rubyforge add_release #{PKG_NAME} #{PKG_NAME} 'REL #{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.gem" + puts release_command + system(release_command) +end \ No newline at end of file diff --git a/vendor/rails/railties/bin/about b/vendor/rails/railties/bin/about new file mode 100644 index 00000000..7b07d46a --- /dev/null +++ b/vendor/rails/railties/bin/about @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/about' \ No newline at end of file diff --git a/vendor/rails/railties/bin/breakpointer b/vendor/rails/railties/bin/breakpointer new file mode 100644 index 00000000..64af76ed --- /dev/null +++ b/vendor/rails/railties/bin/breakpointer @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/breakpointer' \ No newline at end of file diff --git a/vendor/rails/railties/bin/console b/vendor/rails/railties/bin/console new file mode 100644 index 00000000..42f28f7d --- /dev/null +++ b/vendor/rails/railties/bin/console @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/console' \ No newline at end of file diff --git a/vendor/rails/railties/bin/destroy b/vendor/rails/railties/bin/destroy new file mode 100644 index 00000000..fa0e6fcd --- /dev/null +++ b/vendor/rails/railties/bin/destroy @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/destroy' \ No newline at end of file diff --git a/vendor/rails/railties/bin/generate b/vendor/rails/railties/bin/generate new file mode 100644 index 00000000..ef976e09 --- /dev/null +++ b/vendor/rails/railties/bin/generate @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/generate' \ No newline at end of file diff --git a/vendor/rails/railties/bin/performance/benchmarker b/vendor/rails/railties/bin/performance/benchmarker new file mode 100644 index 00000000..c842d35d --- /dev/null +++ b/vendor/rails/railties/bin/performance/benchmarker @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/performance/benchmarker' diff --git a/vendor/rails/railties/bin/performance/profiler b/vendor/rails/railties/bin/performance/profiler new file mode 100644 index 00000000..d855ac8b --- /dev/null +++ b/vendor/rails/railties/bin/performance/profiler @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/performance/profiler' diff --git a/vendor/rails/railties/bin/plugin b/vendor/rails/railties/bin/plugin new file mode 100644 index 00000000..26ca64c0 --- /dev/null +++ b/vendor/rails/railties/bin/plugin @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/plugin' \ No newline at end of file diff --git a/vendor/rails/railties/bin/process/reaper b/vendor/rails/railties/bin/process/reaper new file mode 100644 index 00000000..c77f0453 --- /dev/null +++ b/vendor/rails/railties/bin/process/reaper @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/process/reaper' diff --git a/vendor/rails/railties/bin/process/spawner b/vendor/rails/railties/bin/process/spawner new file mode 100644 index 00000000..7118f398 --- /dev/null +++ b/vendor/rails/railties/bin/process/spawner @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/process/spawner' diff --git a/vendor/rails/railties/bin/rails b/vendor/rails/railties/bin/rails new file mode 100755 index 00000000..ae0cc8ad --- /dev/null +++ b/vendor/rails/railties/bin/rails @@ -0,0 +1,19 @@ +require File.dirname(__FILE__) + '/../lib/ruby_version_check' +Signal.trap("INT") { puts; exit } + +require File.dirname(__FILE__) + '/../lib/rails/version' +if %w(--version -v).include? ARGV.first + puts "Rails #{Rails::VERSION::STRING}" + exit(0) +end + +freeze = ARGV.any? { |option| %w(--freeze -f).include?(option) } +app_path = ARGV.first + +require File.dirname(__FILE__) + '/../lib/rails_generator' + +require 'rails_generator/scripts/generate' +Rails::Generator::Base.use_application_sources! +Rails::Generator::Scripts::Generate.new.run(ARGV, :generator => 'app') + +Dir.chdir(app_path) { `rake rails:freeze:gems`; puts "froze" } if freeze \ No newline at end of file diff --git a/vendor/rails/railties/bin/runner b/vendor/rails/railties/bin/runner new file mode 100644 index 00000000..ccc30f9d --- /dev/null +++ b/vendor/rails/railties/bin/runner @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/runner' \ No newline at end of file diff --git a/vendor/rails/railties/bin/server b/vendor/rails/railties/bin/server new file mode 100644 index 00000000..dfabcb88 --- /dev/null +++ b/vendor/rails/railties/bin/server @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/server' \ No newline at end of file diff --git a/vendor/rails/railties/builtin/rails_info/rails/info.rb b/vendor/rails/railties/builtin/rails_info/rails/info.rb new file mode 100644 index 00000000..89c2ce6e --- /dev/null +++ b/vendor/rails/railties/builtin/rails_info/rails/info.rb @@ -0,0 +1,123 @@ +module Rails + module Info + mattr_accessor :properties + class << (@@properties = []) + def names + map {|(name, )| name} + end + + def value_for(property_name) + find {|(name, )| name == property_name}.last rescue nil + end + end + + class << self #:nodoc: + def property(name, value = nil) + value ||= yield + properties << [name, value] if value + rescue Exception + end + + def components + %w( active_record action_pack action_web_service action_mailer active_support ) + end + + def component_version(component) + require "#{component}/version" + "#{component.classify}::VERSION::STRING".constantize + end + + def edge_rails_revision(info = svn_info) + info[/^Revision: (\d+)/, 1] || freeze_edge_version + end + + def freeze_edge_version + if File.exists?(rails_vendor_root) + begin + Dir[File.join(rails_vendor_root, 'REVISION_*')].first.scan(/_(\d+)$/).first.first + rescue + Dir[File.join(rails_vendor_root, 'TAG_*')].first.scan(/_(.+)$/).first.first rescue 'unknown' + end + end + end + + def to_s + column_width = properties.names.map {|name| name.length}.max + ["About your application's environment", *properties.map do |property| + "%-#{column_width}s %s" % property + end] * "\n" + end + + alias inspect to_s + + def to_html + returning table = '' do + properties.each do |(name, value)| + table << %() + table << %() + end + table << '
      #{CGI.escapeHTML(name.to_s)}#{CGI.escapeHTML(value.to_s)}
      ' + end + end + + protected + def rails_vendor_root + @rails_vendor_root ||= "#{RAILS_ROOT}/vendor/rails" + end + + def svn_info + env_lang, ENV['LC_ALL'] = ENV['LC_ALL'], 'C' + Dir.chdir(rails_vendor_root) do + silence_stderr { `svn info` } + end + ensure + ENV['LC_ALL'] = env_lang + end + end + + # The Ruby version and platform, e.g. "1.8.2 (powerpc-darwin8.2.0)". + property 'Ruby version', "#{RUBY_VERSION} (#{RUBY_PLATFORM})" + + # The RubyGems version, if it's installed. + property 'RubyGems version' do + Gem::RubyGemsVersion + end + + # The Rails version. + property 'Rails version' do + Rails::VERSION::STRING + end + + # Versions of each Rails component (Active Record, Action Pack, + # Action Web Service, Action Mailer, and Active Support). + components.each do |component| + property "#{component.titlecase} version" do + component_version(component) + end + end + + # The Rails SVN revision, if it's checked out into vendor/rails. + property 'Edge Rails revision' do + edge_rails_revision + end + + # The application's location on the filesystem. + property 'Application root' do + File.expand_path(RAILS_ROOT) + end + + # The current Rails environment (development, test, or production). + property 'Environment' do + RAILS_ENV + end + + # The name of the database adapter for the current environment. + property 'Database adapter' do + ActiveRecord::Base.configurations[RAILS_ENV]['adapter'] + end + + property 'Database schema version' do + ActiveRecord::Migrator.current_version rescue nil + end + end +end diff --git a/vendor/rails/railties/builtin/rails_info/rails/info_controller.rb b/vendor/rails/railties/builtin/rails_info/rails/info_controller.rb new file mode 100644 index 00000000..39f8b1f1 --- /dev/null +++ b/vendor/rails/railties/builtin/rails_info/rails/info_controller.rb @@ -0,0 +1,9 @@ +class Rails::InfoController < ActionController::Base + def properties + if local_request? + render :inline => Rails::Info.to_html + else + render :text => '

      For security purposes, this information is only available to local requests.

      ', :status => 500 + end + end +end diff --git a/vendor/rails/railties/builtin/rails_info/rails/info_helper.rb b/vendor/rails/railties/builtin/rails_info/rails/info_helper.rb new file mode 100644 index 00000000..e5605a8d --- /dev/null +++ b/vendor/rails/railties/builtin/rails_info/rails/info_helper.rb @@ -0,0 +1,2 @@ +module Rails::InfoHelper +end diff --git a/vendor/rails/railties/builtin/rails_info/rails_info_controller.rb b/vendor/rails/railties/builtin/rails_info/rails_info_controller.rb new file mode 100644 index 00000000..2009eb3a --- /dev/null +++ b/vendor/rails/railties/builtin/rails_info/rails_info_controller.rb @@ -0,0 +1,2 @@ +# Alias to ensure old public.html still works. +RailsInfoController = Rails::InfoController diff --git a/vendor/rails/railties/configs/apache.conf b/vendor/rails/railties/configs/apache.conf new file mode 100755 index 00000000..d3c99834 --- /dev/null +++ b/vendor/rails/railties/configs/apache.conf @@ -0,0 +1,40 @@ +# General Apache options +AddHandler fastcgi-script .fcgi +AddHandler cgi-script .cgi +Options +FollowSymLinks +ExecCGI + +# If you don't want Rails to look in certain directories, +# use the following rewrite rules so that Apache won't rewrite certain requests +# +# Example: +# RewriteCond %{REQUEST_URI} ^/notrails.* +# RewriteRule .* - [L] + +# Redirect all requests not available on the filesystem to Rails +# By default the cgi dispatcher is used which is very slow +# +# For better performance replace the dispatcher with the fastcgi one +# +# Example: +# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] +RewriteEngine On + +# If your Rails application is accessed via an Alias directive, +# then you MUST also set the RewriteBase in this htaccess file. +# +# Example: +# Alias /myrailsapp /path/to/myrailsapp/public +# RewriteBase /myrailsapp + +RewriteRule ^$ index.html [QSA] +RewriteRule ^([^.]+)$ $1.html [QSA] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^(.*)$ dispatch.cgi [QSA,L] + +# In case Rails experiences terminal errors +# Instead of displaying this message you can supply a file here which will be rendered instead +# +# Example: +# ErrorDocument 500 /500.html + +ErrorDocument 500 "

      Application error

      Rails application failed to start properly" \ No newline at end of file diff --git a/vendor/rails/railties/configs/databases/mysql.yml b/vendor/rails/railties/configs/databases/mysql.yml new file mode 100644 index 00000000..13a54b3c --- /dev/null +++ b/vendor/rails/railties/configs/databases/mysql.yml @@ -0,0 +1,47 @@ +# MySQL (default setup). Versions 4.1 and 5.0 are recommended. +# +# Install the MySQL driver: +# gem install mysql +# On MacOS X: +# gem install mysql -- --include=/usr/local/lib +# On Windows: +# There is no gem for Windows. Install mysql.so from RubyForApache. +# http://rubyforge.org/projects/rubyforapache +# +# And be sure to use new-style password hashing: +# http://dev.mysql.com/doc/refman/5.0/en/old-client.html +development: + adapter: mysql + database: <%= app_name %>_development + username: root + password: +<% if socket -%> + socket: <%= socket %> +<% else -%> + host: localhost +<% end -%> + +# Warning: The database defined as 'test' will be erased and +# re-generated from your development database when you run 'rake'. +# Do not set this db to the same as development or production. +test: + adapter: mysql + database: <%= app_name %>_test + username: root + password: +<% if socket -%> + socket: <%= socket %> +<% else -%> + host: localhost +<% end -%> + +production: + adapter: mysql + database: <%= app_name %>_production + username: root + password: +<% if socket -%> + socket: <%= socket %> +<% else -%> + host: localhost +<% end -%> \ No newline at end of file diff --git a/vendor/rails/railties/configs/databases/oracle.yml b/vendor/rails/railties/configs/databases/oracle.yml new file mode 100644 index 00000000..3c3c8f8b --- /dev/null +++ b/vendor/rails/railties/configs/databases/oracle.yml @@ -0,0 +1,30 @@ +# Oracle/OCI 8i, 9, 10g +# +# Requires Ruby/OCI8: +# http://rubyforge.org/projects/ruby-oci8/ +# +# Specify your database using any valid connection syntax, such as a +# tnsnames.ora service name, or a sql connect url string of the form: +# +# //host:[port][/service name] + +development: + adapter: oracle + database: <%= app_name %>_development + username: <%= app_name %> + password: + +# Warning: The database defined as 'test' will be erased and +# re-generated from your development database when you run 'rake'. +# Do not set this db to the same as development or production. +test: + adapter: oracle + database: <%= app_name %>_test + username: <%= app_name %> + password: + +production: + adapter: oracle + database: <%= app_name %>_production + username: <%= app_name %> + password: diff --git a/vendor/rails/railties/configs/databases/postgresql.yml b/vendor/rails/railties/configs/databases/postgresql.yml new file mode 100644 index 00000000..3c146c13 --- /dev/null +++ b/vendor/rails/railties/configs/databases/postgresql.yml @@ -0,0 +1,44 @@ +# PostgreSQL versions 7.4 - 8.1 +# +# Get the C bindings: +# gem install postgres +# or use the pure-Ruby bindings on Windows: +# gem install postgres-pr +development: + adapter: postgresql + database: <%= app_name %>_development + username: <%= app_name %> + password: + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. Windows does not have + # domain sockets, so uncomment these lines. + #host: localhost + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Character set encoding. The server defaults to sql_ascii. + #encoding: UTF8 + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # info, notice, warning, error, log, fatal, or panic + # The server defaults to notice. + #min_messages: warning + +# Warning: The database defined as 'test' will be erased and +# re-generated from your development database when you run 'rake'. +# Do not set this db to the same as development or production. +test: + adapter: postgresql + database: <%= app_name %>_test + username: <%= app_name %> + password: + +production: + adapter: postgresql + database: <%= app_name %>_production + username: <%= app_name %> + password: diff --git a/vendor/rails/railties/configs/databases/sqlite2.yml b/vendor/rails/railties/configs/databases/sqlite2.yml new file mode 100644 index 00000000..26d3957d --- /dev/null +++ b/vendor/rails/railties/configs/databases/sqlite2.yml @@ -0,0 +1,16 @@ +# SQLite version 2.x +# gem install sqlite-ruby +development: + adapter: sqlite + database: db/development.sqlite2 + +# Warning: The database defined as 'test' will be erased and +# re-generated from your development database when you run 'rake'. +# Do not set this db to the same as development or production. +test: + adapter: sqlite + database: db/test.sqlite2 + +production: + adapter: sqlite + database: db/production.sqlite2 diff --git a/vendor/rails/railties/configs/databases/sqlite3.yml b/vendor/rails/railties/configs/databases/sqlite3.yml new file mode 100644 index 00000000..6f8cbaf1 --- /dev/null +++ b/vendor/rails/railties/configs/databases/sqlite3.yml @@ -0,0 +1,16 @@ +# SQLite version 3.x +# gem install sqlite3-ruby +development: + adapter: sqlite3 + database: db/development.sqlite3 + +# Warning: The database defined as 'test' will be erased and +# re-generated from your development database when you run 'rake'. +# Do not set this db to the same as development or production. +test: + adapter: sqlite3 + database: db/test.sqlite3 + +production: + adapter: sqlite3 + database: db/production.sqlite3 diff --git a/vendor/rails/railties/configs/empty.log b/vendor/rails/railties/configs/empty.log new file mode 100644 index 00000000..e69de29b diff --git a/vendor/rails/railties/configs/lighttpd.conf b/vendor/rails/railties/configs/lighttpd.conf new file mode 100644 index 00000000..c23d1527 --- /dev/null +++ b/vendor/rails/railties/configs/lighttpd.conf @@ -0,0 +1,53 @@ +# Default configuration file for the lighttpd web server +# Start using ./script/server lighttpd + +server.bind = "0.0.0.0" +server.port = 3000 + +server.modules = ( "mod_rewrite", "mod_accesslog", "mod_fastcgi", "mod_compress", "mod_expire" ) + +server.error-handler-404 = "/dispatch.fcgi" +server.document-root = CWD + "/public/" + +server.errorlog = CWD + "/log/lighttpd.error.log" +accesslog.filename = CWD + "/log/lighttpd.access.log" + +url.rewrite = ( "^/$" => "index.html", "^([^.]+)$" => "$1.html" ) + +compress.filetype = ( "text/plain", "text/html", "text/css", "text/javascript" ) +compress.cache-dir = CWD + "/tmp/cache" + +expire.url = ( "/favicon.ico" => "access 3 days", + "/images/" => "access 3 days", + "/stylesheets/" => "access 3 days", + "/javascripts/" => "access 3 days" ) + + +# Change *-procs to 2 if you need to use Upload Progress or other tasks that +# *need* to execute a second request while the first is still pending. +fastcgi.server = ( ".fcgi" => ( "localhost" => ( + "min-procs" => 1, + "max-procs" => 1, + "socket" => CWD + "/tmp/sockets/fcgi.socket", + "bin-path" => CWD + "/public/dispatch.fcgi", + "bin-environment" => ( "RAILS_ENV" => "development" ) +) ) ) + +mimetype.assign = ( + ".css" => "text/css", + ".gif" => "image/gif", + ".htm" => "text/html", + ".html" => "text/html", + ".jpeg" => "image/jpeg", + ".jpg" => "image/jpeg", + ".js" => "text/javascript", + ".png" => "image/png", + ".swf" => "application/x-shockwave-flash", + ".txt" => "text/plain" +) + +# Making sure file uploads above 64k always work when using IE or Safari +# For more information, see http://trac.lighttpd.net/trac/ticket/360 +$HTTP["useragent"] =~ "^(.*MSIE.*)|(.*AppleWebKit.*)$" { + server.max-keep-alive-requests = 0 +} diff --git a/vendor/rails/railties/configs/routes.rb b/vendor/rails/railties/configs/routes.rb new file mode 100644 index 00000000..27fcae80 --- /dev/null +++ b/vendor/rails/railties/configs/routes.rb @@ -0,0 +1,22 @@ +ActionController::Routing::Routes.draw do |map| + # The priority is based upon order of creation: first created -> highest priority. + + # Sample of regular route: + # map.connect 'products/:id', :controller => 'catalog', :action => 'view' + # Keep in mind you can assign values other than :controller and :action + + # Sample of named route: + # map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase' + # This route can be invoked with purchase_url(:id => product.id) + + # You can have the root of your site routed by hooking up '' + # -- just remember to delete public/index.html. + # map.connect '', :controller => "welcome" + + # Allow downloading Web Service WSDL as a file with an extension + # instead of a file named 'wsdl' + map.connect ':controller/service.wsdl', :action => 'wsdl' + + # Install the default route as the lowest priority. + map.connect ':controller/:action/:id' +end diff --git a/vendor/rails/railties/dispatches/dispatch.fcgi b/vendor/rails/railties/dispatches/dispatch.fcgi new file mode 100755 index 00000000..65188f38 --- /dev/null +++ b/vendor/rails/railties/dispatches/dispatch.fcgi @@ -0,0 +1,24 @@ +#!/usr/local/bin/ruby +# +# You may specify the path to the FastCGI crash log (a log of unhandled +# exceptions which forced the FastCGI instance to exit, great for debugging) +# and the number of requests to process before running garbage collection. +# +# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log +# and the GC period is nil (turned off). A reasonable number of requests +# could range from 10-100 depending on the memory footprint of your app. +# +# Example: +# # Default log path, normal GC behavior. +# RailsFCGIHandler.process! +# +# # Default log path, 50 requests between GC. +# RailsFCGIHandler.process! nil, 50 +# +# # Custom log path, normal GC behavior. +# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log' +# +require File.dirname(__FILE__) + "/../config/environment" +require 'fcgi_handler' + +RailsFCGIHandler.process! diff --git a/vendor/rails/railties/dispatches/dispatch.rb b/vendor/rails/railties/dispatches/dispatch.rb new file mode 100755 index 00000000..9b5ae760 --- /dev/null +++ b/vendor/rails/railties/dispatches/dispatch.rb @@ -0,0 +1,10 @@ +#!/usr/local/bin/ruby + +require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) + +# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: +# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired +require "dispatcher" + +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) +Dispatcher.dispatch \ No newline at end of file diff --git a/vendor/rails/railties/dispatches/gateway.cgi b/vendor/rails/railties/dispatches/gateway.cgi new file mode 100644 index 00000000..d21bf099 --- /dev/null +++ b/vendor/rails/railties/dispatches/gateway.cgi @@ -0,0 +1,97 @@ +#!/usr/local/bin/ruby + +require 'drb' + +# This file includes an experimental gateway CGI implementation. It will work +# only on platforms which support both fork and sockets. +# +# To enable it edit public/.htaccess and replace dispatch.cgi with gateway.cgi. +# +# Next, create the directory log/drb_gateway and grant the apache user rw access +# to said directory. +# +# On the next request to your server, the gateway tracker should start up, along +# with a few listener processes. This setup should provide you with much better +# speeds than dispatch.cgi. +# +# Keep in mind that the first request made to the server will be slow, as the +# tracker and listeners will have to load. Also, the tracker and listeners will +# shutdown after a period if inactivity. You can set this value below -- the +# default is 90 seconds. + +TrackerSocket = File.expand_path(File.join(File.dirname(__FILE__), '../log/drb_gateway/tracker.sock')) +DieAfter = 90 # Seconds +Listeners = 3 + +def message(s) + $stderr.puts "gateway.cgi: #{s}" if ENV && ENV["DEBUG_GATEWAY"] +end + +def listener_socket(number) + File.expand_path(File.join(File.dirname(__FILE__), "../log/drb_gateway/listener_#{number}.sock")) +end + +unless File.exists? TrackerSocket + message "Starting tracker and #{Listeners} listeners" + fork do + Process.setsid + STDIN.reopen "/dev/null" + STDOUT.reopen "/dev/null", "a" + + root = File.expand_path(File.dirname(__FILE__) + '/..') + + message "starting tracker" + fork do + ARGV.clear + ARGV << TrackerSocket << Listeners.to_s << DieAfter.to_s + load File.join(root, 'script', 'tracker') + end + + message "starting listeners" + require File.join(root, 'config/environment.rb') + Listeners.times do |number| + fork do + ARGV.clear + ARGV << listener_socket(number) << DieAfter.to_s + load File.join(root, 'script', 'listener') + end + end + end + + message "waiting for tracker and listener to arise..." + ready = false + 10.times do + sleep 0.5 + break if (ready = File.exists?(TrackerSocket) && File.exists?(listener_socket(0))) + end + + if ready + message "tracker and listener are ready" + else + message "Waited 5 seconds, listener and tracker not ready... dropping request" + Kernel.exit 1 + end +end + +DRb.start_service + +message "connecting to tracker" +tracker = DRbObject.new_with_uri("drbunix:#{TrackerSocket}") + +input = $stdin.read +$stdin.close + +env = ENV.inspect + +output = nil +tracker.with_listener do |number| + message "connecting to listener #{number}" + socket = listener_socket(number) + listener = DRbObject.new_with_uri("drbunix:#{socket}") + output = listener.process(env, input) + message "listener #{number} has finished, writing output" +end + +$stdout.write output +$stdout.flush +$stdout.close \ No newline at end of file diff --git a/vendor/rails/railties/doc/README_FOR_APP b/vendor/rails/railties/doc/README_FOR_APP new file mode 100644 index 00000000..ac6c1491 --- /dev/null +++ b/vendor/rails/railties/doc/README_FOR_APP @@ -0,0 +1,2 @@ +Use this README file to introduce your application and point to useful places in the API for learning more. +Run "rake appdoc" to generate API documentation for your models and controllers. \ No newline at end of file diff --git a/vendor/rails/railties/environments/boot.rb b/vendor/rails/railties/environments/boot.rb new file mode 100644 index 00000000..9a094cbc --- /dev/null +++ b/vendor/rails/railties/environments/boot.rb @@ -0,0 +1,44 @@ +# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb + +unless defined?(RAILS_ROOT) + root_path = File.join(File.dirname(__FILE__), '..') + + unless RUBY_PLATFORM =~ /mswin32/ + require 'pathname' + root_path = Pathname.new(root_path).cleanpath(true).to_s + end + + RAILS_ROOT = root_path +end + +unless defined?(Rails::Initializer) + if File.directory?("#{RAILS_ROOT}/vendor/rails") + require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" + else + require 'rubygems' + + environment_without_comments = IO.readlines(File.dirname(__FILE__) + '/environment.rb').reject { |l| l =~ /^#/ }.join + environment_without_comments =~ /[^#]RAILS_GEM_VERSION = '([\d.]+)'/ + rails_gem_version = $1 + + if version = defined?(RAILS_GEM_VERSION) ? RAILS_GEM_VERSION : rails_gem_version + rails_gem = Gem.cache.search('rails', "=#{version}").first + + if rails_gem + require_gem "rails", "=#{version}" + require rails_gem.full_gem_path + '/lib/initializer' + else + STDERR.puts %(Cannot find gem for Rails =#{version}: + Install the missing gem with 'gem install -v=#{version} rails', or + change environment.rb to define RAILS_GEM_VERSION with your desired version. + ) + exit 1 + end + else + require_gem "rails" + require 'initializer' + end + end + + Rails::Initializer.run(:set_load_path) +end \ No newline at end of file diff --git a/vendor/rails/railties/environments/development.rb b/vendor/rails/railties/environments/development.rb new file mode 100644 index 00000000..0589aa97 --- /dev/null +++ b/vendor/rails/railties/environments/development.rb @@ -0,0 +1,21 @@ +# Settings specified here will take precedence over those in config/environment.rb + +# In the development environment your application's code is reloaded on +# every request. This slows down response time but is perfect for development +# since you don't have to restart the webserver when you make code changes. +config.cache_classes = false + +# Log error messages when you accidentally call methods on nil. +config.whiny_nils = true + +# Enable the breakpoint server that script/breakpointer connects to +config.breakpoint_server = true + +# Show full error reports and disable caching +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false +config.action_view.cache_template_extensions = false +config.action_view.debug_rjs = true + +# Don't care if the mailer can't send +config.action_mailer.raise_delivery_errors = false diff --git a/vendor/rails/railties/environments/environment.rb b/vendor/rails/railties/environments/environment.rb new file mode 100644 index 00000000..0620d3ed --- /dev/null +++ b/vendor/rails/railties/environments/environment.rb @@ -0,0 +1,53 @@ +# Be sure to restart your web server when you modify this file. + +# Uncomment below to force Rails into production mode when +# you don't control web/app server and can't set it the proper way +# ENV['RAILS_ENV'] ||= 'production' + +# Specifies gem version of Rails to use when vendor/rails is not present +<%= '# ' if freeze %>RAILS_GEM_VERSION = '<%= Rails::VERSION::STRING %>' + +# Bootstrap the Rails environment, frameworks, and default configuration +require File.join(File.dirname(__FILE__), 'boot') + +Rails::Initializer.run do |config| + # Settings in config/environments/* take precedence those specified here + + # Skip frameworks you're not going to use (only works if using vendor/rails) + # config.frameworks -= [ :action_web_service, :action_mailer ] + + # Add additional load paths for your own custom dirs + # config.load_paths += %W( #{RAILS_ROOT}/extras ) + + # Force all environments to use the same logger level + # (by default production uses :info, the others :debug) + # config.log_level = :debug + + # Use the database for sessions instead of the file system + # (create the session table with 'rake db:sessions:create') + # config.action_controller.session_store = :active_record_store + + # Use SQL instead of Active Record's schema dumper when creating the test database. + # This is necessary if your schema can't be completely dumped by the schema dumper, + # like if you have constraints or database-specific column types + # config.active_record.schema_format = :sql + + # Activate observers that should always be running + # config.active_record.observers = :cacher, :garbage_collector + + # Make Active Record use UTC-base instead of local time + # config.active_record.default_timezone = :utc + + # See Rails::Configuration for more options +end + +# Add new inflection rules using the following format +# (all these examples are active by default): +# Inflector.inflections do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# Include your application configuration below \ No newline at end of file diff --git a/vendor/rails/railties/environments/production.rb b/vendor/rails/railties/environments/production.rb new file mode 100644 index 00000000..5a4e2b1c --- /dev/null +++ b/vendor/rails/railties/environments/production.rb @@ -0,0 +1,18 @@ +# Settings specified here will take precedence over those in config/environment.rb + +# The production environment is meant for finished, "live" apps. +# Code is not reloaded between requests +config.cache_classes = true + +# Use a different logger for distributed setups +# config.logger = SyslogLogger.new + +# Full error reports are disabled and caching is turned on +config.action_controller.consider_all_requests_local = false +config.action_controller.perform_caching = true + +# Enable serving of images, stylesheets, and javascripts from an asset server +# config.action_controller.asset_host = "http://assets.example.com" + +# Disable delivery errors if you bad email addresses should just be ignored +# config.action_mailer.raise_delivery_errors = false diff --git a/vendor/rails/railties/environments/test.rb b/vendor/rails/railties/environments/test.rb new file mode 100644 index 00000000..f0689b92 --- /dev/null +++ b/vendor/rails/railties/environments/test.rb @@ -0,0 +1,19 @@ +# Settings specified here will take precedence over those in config/environment.rb + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! +config.cache_classes = true + +# Log error messages when you accidentally call methods on nil. +config.whiny_nils = true + +# Show full error reports and disable caching +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false + +# Tell ActionMailer not to deliver emails to the real world. +# The :test delivery method accumulates sent emails in the +# ActionMailer::Base.deliveries array. +config.action_mailer.delivery_method = :test \ No newline at end of file diff --git a/vendor/rails/railties/fresh_rakefile b/vendor/rails/railties/fresh_rakefile new file mode 100755 index 00000000..3bb0e859 --- /dev/null +++ b/vendor/rails/railties/fresh_rakefile @@ -0,0 +1,10 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require(File.join(File.dirname(__FILE__), 'config', 'boot')) + +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +require 'tasks/rails' diff --git a/vendor/rails/railties/helpers/application.rb b/vendor/rails/railties/helpers/application.rb new file mode 100644 index 00000000..537de40d --- /dev/null +++ b/vendor/rails/railties/helpers/application.rb @@ -0,0 +1,4 @@ +# Filters added to this controller will be run for all controllers in the application. +# Likewise, all the methods added will be available for all controllers. +class ApplicationController < ActionController::Base +end \ No newline at end of file diff --git a/vendor/rails/railties/helpers/application_helper.rb b/vendor/rails/railties/helpers/application_helper.rb new file mode 100644 index 00000000..22a7940e --- /dev/null +++ b/vendor/rails/railties/helpers/application_helper.rb @@ -0,0 +1,3 @@ +# Methods added to this helper will be available to all templates in the application. +module ApplicationHelper +end diff --git a/vendor/rails/railties/helpers/test_helper.rb b/vendor/rails/railties/helpers/test_helper.rb new file mode 100644 index 00000000..a299c7f6 --- /dev/null +++ b/vendor/rails/railties/helpers/test_helper.rb @@ -0,0 +1,28 @@ +ENV["RAILS_ENV"] = "test" +require File.expand_path(File.dirname(__FILE__) + "/../config/environment") +require 'test_help' + +class Test::Unit::TestCase + # Transactional fixtures accelerate your tests by wrapping each test method + # in a transaction that's rolled back on completion. This ensures that the + # test database remains unchanged so your fixtures don't have to be reloaded + # between every test method. Fewer database queries means faster tests. + # + # Read Mike Clark's excellent walkthrough at + # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting + # + # Every Active Record database supports transactions except MyISAM tables + # in MySQL. Turn off transactional fixtures in this case; however, if you + # don't care one way or the other, switching from MyISAM to InnoDB tables + # is recommended. + self.use_transactional_fixtures = true + + # Instantiated fixtures are slow, but give you @david where otherwise you + # would need people(:david). If you don't want to migrate your existing + # test cases which use the @david style and don't mind the speed hit (each + # instantiated fixtures translates to a database query per test method), + # then set this back to true. + self.use_instantiated_fixtures = false + + # Add more helper methods to be used by all tests here... +end diff --git a/vendor/rails/railties/html/404.html b/vendor/rails/railties/html/404.html new file mode 100644 index 00000000..0e184561 --- /dev/null +++ b/vendor/rails/railties/html/404.html @@ -0,0 +1,8 @@ + + + +

      File not found

      +

      Change this error message for pages not found in public/404.html

      + + \ No newline at end of file diff --git a/vendor/rails/railties/html/500.html b/vendor/rails/railties/html/500.html new file mode 100644 index 00000000..ab95f74c --- /dev/null +++ b/vendor/rails/railties/html/500.html @@ -0,0 +1,8 @@ + + + +

      Application error

      +

      Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html

      + + \ No newline at end of file diff --git a/vendor/rails/railties/html/favicon.ico b/vendor/rails/railties/html/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/vendor/rails/railties/html/images/rails.png b/vendor/rails/railties/html/images/rails.png new file mode 100644 index 00000000..b8441f18 Binary files /dev/null and b/vendor/rails/railties/html/images/rails.png differ diff --git a/vendor/rails/railties/html/index.html b/vendor/rails/railties/html/index.html new file mode 100644 index 00000000..a2daab72 --- /dev/null +++ b/vendor/rails/railties/html/index.html @@ -0,0 +1,277 @@ + + + + + Ruby on Rails: Welcome aboard + + + + + + +
      + + +
      + + + + +
      +

      Getting started

      +

      Here’s how to get rolling:

      + +
        +
      1. +

        Create your databases and edit config/database.yml

        +

        Rails needs to know your login and password.

        +
      2. + +
      3. +

        Use script/generate to create your models and controllers

        +

        To see all available options, run it without parameters.

        +
      4. + +
      5. +

        Set up a default route and remove or rename this file

        +

        Routes are setup in config/routes.rb.

        +
      6. +
      +
      +
      + + +
      + + \ No newline at end of file diff --git a/vendor/rails/railties/html/javascripts/application.js b/vendor/rails/railties/html/javascripts/application.js new file mode 100644 index 00000000..fe457769 --- /dev/null +++ b/vendor/rails/railties/html/javascripts/application.js @@ -0,0 +1,2 @@ +// Place your application-specific JavaScript functions and classes here +// This file is automatically included by javascript_include_tag :defaults diff --git a/vendor/rails/railties/html/javascripts/controls.js b/vendor/rails/railties/html/javascripts/controls.js new file mode 100644 index 00000000..de0261ed --- /dev/null +++ b/vendor/rails/railties/html/javascripts/controls.js @@ -0,0 +1,815 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005 Jon Tirsen (http://www.tirsen.com) +// Contributors: +// Richard Livsey +// Rahul Bhargava +// Rob Wills +// +// See scriptaculous.js for full license. + +// Autocompleter.Base handles all the autocompletion functionality +// that's independent of the data source for autocompletion. This +// includes drawing the autocompletion menu, observing keyboard +// and mouse events, and similar. +// +// Specific autocompleters need to provide, at the very least, +// a getUpdatedChoices function that will be invoked every time +// the text inside the monitored textbox changes. This method +// should get the text for which to provide autocompletion by +// invoking this.getToken(), NOT by directly accessing +// this.element.value. This is to allow incremental tokenized +// autocompletion. Specific auto-completion logic (AJAX, etc) +// belongs in getUpdatedChoices. +// +// Tokenized incremental autocompletion is enabled automatically +// when an autocompleter is instantiated with the 'tokens' option +// in the options parameter, e.g.: +// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); +// will incrementally autocomplete with a comma as the token. +// Additionally, ',' in the above example can be replaced with +// a token array, e.g. { tokens: [',', '\n'] } which +// enables autocompletion on multiple tokens. This is most +// useful when one of the tokens is \n (a newline), as it +// allows smart autocompletion after linebreaks. + +var Autocompleter = {} +Autocompleter.Base = function() {}; +Autocompleter.Base.prototype = { + baseInitialize: function(element, update, options) { + this.element = $(element); + this.update = $(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entryCount = 0; + + if (this.setOptions) + this.setOptions(options); + else + this.options = options || {}; + + this.options.paramName = this.options.paramName || this.element.name; + this.options.tokens = this.options.tokens || []; + this.options.frequency = this.options.frequency || 0.4; + this.options.minChars = this.options.minChars || 1; + this.options.onShow = this.options.onShow || + function(element, update){ + if(!update.style.position || update.style.position=='absolute') { + update.style.position = 'absolute'; + Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight}); + } + Effect.Appear(update,{duration:0.15}); + }; + this.options.onHide = this.options.onHide || + function(element, update){ new Effect.Fade(update,{duration:0.15}) }; + + if (typeof(this.options.tokens) == 'string') + this.options.tokens = new Array(this.options.tokens); + + this.observer = null; + + this.element.setAttribute('autocomplete','off'); + + Element.hide(this.update); + + Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); + }, + + show: function() { + if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && + (navigator.appVersion.indexOf('MSIE')>0) && + (navigator.userAgent.indexOf('Opera')<0) && + (Element.getStyle(this.update, 'position')=='absolute')) { + new Insertion.After(this.update, + ''); + this.iefix = $(this.update.id+'_iefix'); + } + if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); + }, + + fixIEOverlapping: function() { + Position.clone(this.update, this.iefix); + this.iefix.style.zIndex = 1; + this.update.style.zIndex = 2; + Element.show(this.iefix); + }, + + hide: function() { + this.stopIndicator(); + if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); + if(this.iefix) Element.hide(this.iefix); + }, + + startIndicator: function() { + if(this.options.indicator) Element.show(this.options.indicator); + }, + + stopIndicator: function() { + if(this.options.indicator) Element.hide(this.options.indicator); + }, + + onKeyPress: function(event) { + if(this.active) + switch(event.keyCode) { + case Event.KEY_TAB: + case Event.KEY_RETURN: + this.selectEntry(); + Event.stop(event); + case Event.KEY_ESC: + this.hide(); + this.active = false; + Event.stop(event); + return; + case Event.KEY_LEFT: + case Event.KEY_RIGHT: + return; + case Event.KEY_UP: + this.markPrevious(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + case Event.KEY_DOWN: + this.markNext(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || + (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return; + + this.changed = true; + this.hasFocus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); + }, + + activate: function() { + this.changed = false; + this.hasFocus = true; + this.getUpdatedChoices(); + }, + + onHover: function(event) { + var element = Event.findElement(event, 'LI'); + if(this.index != element.autocompleteIndex) + { + this.index = element.autocompleteIndex; + this.render(); + } + Event.stop(event); + }, + + onClick: function(event) { + var element = Event.findElement(event, 'LI'); + this.index = element.autocompleteIndex; + this.selectEntry(); + this.hide(); + }, + + onBlur: function(event) { + // needed to make click events working + setTimeout(this.hide.bind(this), 250); + this.hasFocus = false; + this.active = false; + }, + + render: function() { + if(this.entryCount > 0) { + for (var i = 0; i < this.entryCount; i++) + this.index==i ? + Element.addClassName(this.getEntry(i),"selected") : + Element.removeClassName(this.getEntry(i),"selected"); + + if(this.hasFocus) { + this.show(); + this.active = true; + } + } else { + this.active = false; + this.hide(); + } + }, + + markPrevious: function() { + if(this.index > 0) this.index-- + else this.index = this.entryCount-1; + }, + + markNext: function() { + if(this.index < this.entryCount-1) this.index++ + else this.index = 0; + }, + + getEntry: function(index) { + return this.update.firstChild.childNodes[index]; + }, + + getCurrentEntry: function() { + return this.getEntry(this.index); + }, + + selectEntry: function() { + this.active = false; + this.updateElement(this.getCurrentEntry()); + }, + + updateElement: function(selectedElement) { + if (this.options.updateElement) { + this.options.updateElement(selectedElement); + return; + } + var value = ''; + if (this.options.select) { + var nodes = document.getElementsByClassName(this.options.select, selectedElement) || []; + if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); + } else + value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + + var lastTokenPos = this.findLastToken(); + if (lastTokenPos != -1) { + var newValue = this.element.value.substr(0, lastTokenPos + 1); + var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/); + if (whitespace) + newValue += whitespace[0]; + this.element.value = newValue + value; + } else { + this.element.value = value; + } + this.element.focus(); + + if (this.options.afterUpdateElement) + this.options.afterUpdateElement(this.element, selectedElement); + }, + + updateChoices: function(choices) { + if(!this.changed && this.hasFocus) { + this.update.innerHTML = choices; + Element.cleanWhitespace(this.update); + Element.cleanWhitespace(this.update.firstChild); + + if(this.update.firstChild && this.update.firstChild.childNodes) { + this.entryCount = + this.update.firstChild.childNodes.length; + for (var i = 0; i < this.entryCount; i++) { + var entry = this.getEntry(i); + entry.autocompleteIndex = i; + this.addObservers(entry); + } + } else { + this.entryCount = 0; + } + + this.stopIndicator(); + + this.index = 0; + this.render(); + } + }, + + addObservers: function(element) { + Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); + Event.observe(element, "click", this.onClick.bindAsEventListener(this)); + }, + + onObserverEvent: function() { + this.changed = false; + if(this.getToken().length>=this.options.minChars) { + this.startIndicator(); + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + }, + + getToken: function() { + var tokenPos = this.findLastToken(); + if (tokenPos != -1) + var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,''); + else + var ret = this.element.value; + + return /\n/.test(ret) ? '' : ret; + }, + + findLastToken: function() { + var lastTokenPos = -1; + + for (var i=0; i lastTokenPos) + lastTokenPos = thisTokenPos; + } + return lastTokenPos; + } +} + +Ajax.Autocompleter = Class.create(); +Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), { + initialize: function(element, update, url, options) { + this.baseInitialize(element, update, options); + this.options.asynchronous = true; + this.options.onComplete = this.onComplete.bind(this); + this.options.defaultParams = this.options.parameters || null; + this.url = url; + }, + + getUpdatedChoices: function() { + entry = encodeURIComponent(this.options.paramName) + '=' + + encodeURIComponent(this.getToken()); + + this.options.parameters = this.options.callback ? + this.options.callback(this.element, entry) : entry; + + if(this.options.defaultParams) + this.options.parameters += '&' + this.options.defaultParams; + + new Ajax.Request(this.url, this.options); + }, + + onComplete: function(request) { + this.updateChoices(request.responseText); + } + +}); + +// The local array autocompleter. Used when you'd prefer to +// inject an array of autocompletion options into the page, rather +// than sending out Ajax queries, which can be quite slow sometimes. +// +// The constructor takes four parameters. The first two are, as usual, +// the id of the monitored textbox, and id of the autocompletion menu. +// The third is the array you want to autocomplete from, and the fourth +// is the options block. +// +// Extra local autocompletion options: +// - choices - How many autocompletion choices to offer +// +// - partialSearch - If false, the autocompleter will match entered +// text only at the beginning of strings in the +// autocomplete array. Defaults to true, which will +// match text at the beginning of any *word* in the +// strings in the autocomplete array. If you want to +// search anywhere in the string, additionally set +// the option fullSearch to true (default: off). +// +// - fullSsearch - Search anywhere in autocomplete array strings. +// +// - partialChars - How many characters to enter before triggering +// a partial match (unlike minChars, which defines +// how many characters are required to do any match +// at all). Defaults to 2. +// +// - ignoreCase - Whether to ignore case when autocompleting. +// Defaults to true. +// +// It's possible to pass in a custom function as the 'selector' +// option, if you prefer to write your own autocompletion logic. +// In that case, the other options above will not apply unless +// you support them. + +Autocompleter.Local = Class.create(); +Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { + initialize: function(element, update, array, options) { + this.baseInitialize(element, update, options); + this.options.array = array; + }, + + getUpdatedChoices: function() { + this.updateChoices(this.options.selector(this)); + }, + + setOptions: function(options) { + this.options = Object.extend({ + choices: 10, + partialSearch: true, + partialChars: 2, + ignoreCase: true, + fullSearch: false, + selector: function(instance) { + var ret = []; // Beginning matches + var partial = []; // Inside matches + var entry = instance.getToken(); + var count = 0; + + for (var i = 0; i < instance.options.array.length && + ret.length < instance.options.choices ; i++) { + + var elem = instance.options.array[i]; + var foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : + elem.indexOf(entry); + + while (foundPos != -1) { + if (foundPos == 0 && elem.length != entry.length) { + ret.push("
    • " + elem.substr(0, entry.length) + "" + + elem.substr(entry.length) + "
    • "); + break; + } else if (entry.length >= instance.options.partialChars && + instance.options.partialSearch && foundPos != -1) { + if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { + partial.push("
    • " + elem.substr(0, foundPos) + "" + + elem.substr(foundPos, entry.length) + "" + elem.substr( + foundPos + entry.length) + "
    • "); + break; + } + } + + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + elem.indexOf(entry, foundPos + 1); + + } + } + if (partial.length) + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) + return "
        " + ret.join('') + "
      "; + } + }, options || {}); + } +}); + +// AJAX in-place editor +// +// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor + +// Use this if you notice weird scrolling problems on some browsers, +// the DOM might be a bit confused when this gets called so do this +// waits 1 ms (with setTimeout) until it does the activation +Field.scrollFreeActivate = function(field) { + setTimeout(function() { + Field.activate(field); + }, 1); +} + +Ajax.InPlaceEditor = Class.create(); +Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99"; +Ajax.InPlaceEditor.prototype = { + initialize: function(element, url, options) { + this.url = url; + this.element = $(element); + + this.options = Object.extend({ + okButton: true, + okText: "ok", + cancelLink: true, + cancelText: "cancel", + savingText: "Saving...", + clickToEditText: "Click to edit", + okText: "ok", + rows: 1, + onComplete: function(transport, element) { + new Effect.Highlight(element, {startcolor: this.options.highlightcolor}); + }, + onFailure: function(transport) { + alert("Error communicating with the server: " + transport.responseText.stripTags()); + }, + callback: function(form) { + return Form.serialize(form); + }, + handleLineBreaks: true, + loadingText: 'Loading...', + savingClassName: 'inplaceeditor-saving', + loadingClassName: 'inplaceeditor-loading', + formClassName: 'inplaceeditor-form', + highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, + highlightendcolor: "#FFFFFF", + externalControl: null, + submitOnBlur: false, + ajaxOptions: {}, + evalScripts: false + }, options || {}); + + if(!this.options.formId && this.element.id) { + this.options.formId = this.element.id + "-inplaceeditor"; + if ($(this.options.formId)) { + // there's already a form with that name, don't specify an id + this.options.formId = null; + } + } + + if (this.options.externalControl) { + this.options.externalControl = $(this.options.externalControl); + } + + this.originalBackground = Element.getStyle(this.element, 'background-color'); + if (!this.originalBackground) { + this.originalBackground = "transparent"; + } + + this.element.title = this.options.clickToEditText; + + this.onclickListener = this.enterEditMode.bindAsEventListener(this); + this.mouseoverListener = this.enterHover.bindAsEventListener(this); + this.mouseoutListener = this.leaveHover.bindAsEventListener(this); + Event.observe(this.element, 'click', this.onclickListener); + Event.observe(this.element, 'mouseover', this.mouseoverListener); + Event.observe(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.observe(this.options.externalControl, 'click', this.onclickListener); + Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + }, + enterEditMode: function(evt) { + if (this.saving) return; + if (this.editing) return; + this.editing = true; + this.onEnterEditMode(); + if (this.options.externalControl) { + Element.hide(this.options.externalControl); + } + Element.hide(this.element); + this.createForm(); + this.element.parentNode.insertBefore(this.form, this.element); + Field.scrollFreeActivate(this.editField); + // stop the event to avoid a page refresh in Safari + if (evt) { + Event.stop(evt); + } + return false; + }, + createForm: function() { + this.form = document.createElement("form"); + this.form.id = this.options.formId; + Element.addClassName(this.form, this.options.formClassName) + this.form.onsubmit = this.onSubmit.bind(this); + + this.createEditField(); + + if (this.options.textarea) { + var br = document.createElement("br"); + this.form.appendChild(br); + } + + if (this.options.okButton) { + okButton = document.createElement("input"); + okButton.type = "submit"; + okButton.value = this.options.okText; + okButton.className = 'editor_ok_button'; + this.form.appendChild(okButton); + } + + if (this.options.cancelLink) { + cancelLink = document.createElement("a"); + cancelLink.href = "#"; + cancelLink.appendChild(document.createTextNode(this.options.cancelText)); + cancelLink.onclick = this.onclickCancel.bind(this); + cancelLink.className = 'editor_cancel'; + this.form.appendChild(cancelLink); + } + }, + hasHTMLLineBreaks: function(string) { + if (!this.options.handleLineBreaks) return false; + return string.match(/
      /i); + }, + convertHTMLLineBreaks: function(string) { + return string.replace(/
      /gi, "\n").replace(//gi, "\n").replace(/<\/p>/gi, "\n").replace(/

      /gi, ""); + }, + createEditField: function() { + var text; + if(this.options.loadTextURL) { + text = this.options.loadingText; + } else { + text = this.getText(); + } + + var obj = this; + + if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { + this.options.textarea = false; + var textField = document.createElement("input"); + textField.obj = this; + textField.type = "text"; + textField.name = "value"; + textField.value = text; + textField.style.backgroundColor = this.options.highlightcolor; + textField.className = 'editor_field'; + var size = this.options.size || this.options.cols || 0; + if (size != 0) textField.size = size; + if (this.options.submitOnBlur) + textField.onblur = this.onSubmit.bind(this); + this.editField = textField; + } else { + this.options.textarea = true; + var textArea = document.createElement("textarea"); + textArea.obj = this; + textArea.name = "value"; + textArea.value = this.convertHTMLLineBreaks(text); + textArea.rows = this.options.rows; + textArea.cols = this.options.cols || 40; + textArea.className = 'editor_field'; + if (this.options.submitOnBlur) + textArea.onblur = this.onSubmit.bind(this); + this.editField = textArea; + } + + if(this.options.loadTextURL) { + this.loadExternalText(); + } + this.form.appendChild(this.editField); + }, + getText: function() { + return this.element.innerHTML; + }, + loadExternalText: function() { + Element.addClassName(this.form, this.options.loadingClassName); + this.editField.disabled = true; + new Ajax.Request( + this.options.loadTextURL, + Object.extend({ + asynchronous: true, + onComplete: this.onLoadedExternalText.bind(this) + }, this.options.ajaxOptions) + ); + }, + onLoadedExternalText: function(transport) { + Element.removeClassName(this.form, this.options.loadingClassName); + this.editField.disabled = false; + this.editField.value = transport.responseText.stripTags(); + }, + onclickCancel: function() { + this.onComplete(); + this.leaveEditMode(); + return false; + }, + onFailure: function(transport) { + this.options.onFailure(transport); + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + this.oldInnerHTML = null; + } + return false; + }, + onSubmit: function() { + // onLoading resets these so we need to save them away for the Ajax call + var form = this.form; + var value = this.editField.value; + + // do this first, sometimes the ajax call returns before we get a chance to switch on Saving... + // which means this will actually switch on Saving... *after* we've left edit mode causing Saving... + // to be displayed indefinitely + this.onLoading(); + + if (this.options.evalScripts) { + new Ajax.Request( + this.url, Object.extend({ + parameters: this.options.callback(form, value), + onComplete: this.onComplete.bind(this), + onFailure: this.onFailure.bind(this), + asynchronous:true, + evalScripts:true + }, this.options.ajaxOptions)); + } else { + new Ajax.Updater( + { success: this.element, + // don't update on failure (this could be an option) + failure: null }, + this.url, Object.extend({ + parameters: this.options.callback(form, value), + onComplete: this.onComplete.bind(this), + onFailure: this.onFailure.bind(this) + }, this.options.ajaxOptions)); + } + // stop the event to avoid a page refresh in Safari + if (arguments.length > 1) { + Event.stop(arguments[0]); + } + return false; + }, + onLoading: function() { + this.saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + showSaving: function() { + this.oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + Element.addClassName(this.element, this.options.savingClassName); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + }, + removeForm: function() { + if(this.form) { + if (this.form.parentNode) Element.remove(this.form); + this.form = null; + } + }, + enterHover: function() { + if (this.saving) return; + this.element.style.backgroundColor = this.options.highlightcolor; + if (this.effect) { + this.effect.cancel(); + } + Element.addClassName(this.element, this.options.hoverClassName) + }, + leaveHover: function() { + if (this.options.backgroundColor) { + this.element.style.backgroundColor = this.oldBackground; + } + Element.removeClassName(this.element, this.options.hoverClassName) + if (this.saving) return; + this.effect = new Effect.Highlight(this.element, { + startcolor: this.options.highlightcolor, + endcolor: this.options.highlightendcolor, + restorecolor: this.originalBackground + }); + }, + leaveEditMode: function() { + Element.removeClassName(this.element, this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + if (this.options.externalControl) { + Element.show(this.options.externalControl); + } + this.editing = false; + this.saving = false; + this.oldInnerHTML = null; + this.onLeaveEditMode(); + }, + onComplete: function(transport) { + this.leaveEditMode(); + this.options.onComplete.bind(this)(transport, this.element); + }, + onEnterEditMode: function() {}, + onLeaveEditMode: function() {}, + dispose: function() { + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + } + this.leaveEditMode(); + Event.stopObserving(this.element, 'click', this.onclickListener); + Event.stopObserving(this.element, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.stopObserving(this.options.externalControl, 'click', this.onclickListener); + Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + } +}; + +Ajax.InPlaceCollectionEditor = Class.create(); +Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype); +Object.extend(Ajax.InPlaceCollectionEditor.prototype, { + createEditField: function() { + if (!this.cached_selectTag) { + var selectTag = document.createElement("select"); + var collection = this.options.collection || []; + var optionTag; + collection.each(function(e,i) { + optionTag = document.createElement("option"); + optionTag.value = (e instanceof Array) ? e[0] : e; + if(this.options.value==optionTag.value) optionTag.selected = true; + optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e)); + selectTag.appendChild(optionTag); + }.bind(this)); + this.cached_selectTag = selectTag; + } + + this.editField = this.cached_selectTag; + if(this.options.loadTextURL) this.loadExternalText(); + this.form.appendChild(this.editField); + this.options.callback = function(form, value) { + return "value=" + encodeURIComponent(value); + } + } +}); + +// Delayed observer, like Form.Element.Observer, +// but waits for delay after last key input +// Ideal for live-search fields + +Form.Element.DelayedObserver = Class.create(); +Form.Element.DelayedObserver.prototype = { + initialize: function(element, delay, callback) { + this.delay = delay || 0.5; + this.element = $(element); + this.callback = callback; + this.timer = null; + this.lastValue = $F(this.element); + Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); + }, + delayedListener: function(event) { + if(this.lastValue == $F(this.element)) return; + if(this.timer) clearTimeout(this.timer); + this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); + this.lastValue = $F(this.element); + }, + onTimerEvent: function() { + this.timer = null; + this.callback(this.element, $F(this.element)); + } +}; diff --git a/vendor/rails/railties/html/javascripts/dragdrop.js b/vendor/rails/railties/html/javascripts/dragdrop.js new file mode 100644 index 00000000..a01b7be2 --- /dev/null +++ b/vendor/rails/railties/html/javascripts/dragdrop.js @@ -0,0 +1,913 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) +// +// See scriptaculous.js for full license. + +/*--------------------------------------------------------------------------*/ + +var Droppables = { + drops: [], + + remove: function(element) { + this.drops = this.drops.reject(function(d) { return d.element==$(element) }); + }, + + add: function(element) { + element = $(element); + var options = Object.extend({ + greedy: true, + hoverclass: null, + tree: false + }, arguments[1] || {}); + + // cache containers + if(options.containment) { + options._containers = []; + var containment = options.containment; + if((typeof containment == 'object') && + (containment.constructor == Array)) { + containment.each( function(c) { options._containers.push($(c)) }); + } else { + options._containers.push($(containment)); + } + } + + if(options.accept) options.accept = [options.accept].flatten(); + + Element.makePositioned(element); // fix IE + options.element = element; + + this.drops.push(options); + }, + + findDeepestChild: function(drops) { + deepest = drops[0]; + + for (i = 1; i < drops.length; ++i) + if (Element.isParent(drops[i].element, deepest.element)) + deepest = drops[i]; + + return deepest; + }, + + isContained: function(element, drop) { + var containmentNode; + if(drop.tree) { + containmentNode = element.treeNode; + } else { + containmentNode = element.parentNode; + } + return drop._containers.detect(function(c) { return containmentNode == c }); + }, + + isAffected: function(point, element, drop) { + return ( + (drop.element!=element) && + ((!drop._containers) || + this.isContained(element, drop)) && + ((!drop.accept) || + (Element.classNames(element).detect( + function(v) { return drop.accept.include(v) } ) )) && + Position.within(drop.element, point[0], point[1]) ); + }, + + deactivate: function(drop) { + if(drop.hoverclass) + Element.removeClassName(drop.element, drop.hoverclass); + this.last_active = null; + }, + + activate: function(drop) { + if(drop.hoverclass) + Element.addClassName(drop.element, drop.hoverclass); + this.last_active = drop; + }, + + show: function(point, element) { + if(!this.drops.length) return; + var affected = []; + + if(this.last_active) this.deactivate(this.last_active); + this.drops.each( function(drop) { + if(Droppables.isAffected(point, element, drop)) + affected.push(drop); + }); + + if(affected.length>0) { + drop = Droppables.findDeepestChild(affected); + Position.within(drop.element, point[0], point[1]); + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + + Droppables.activate(drop); + } + }, + + fire: function(event, element) { + if(!this.last_active) return; + Position.prepare(); + + if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) + if (this.last_active.onDrop) + this.last_active.onDrop(element, this.last_active.element, event); + }, + + reset: function() { + if(this.last_active) + this.deactivate(this.last_active); + } +} + +var Draggables = { + drags: [], + observers: [], + + register: function(draggable) { + if(this.drags.length == 0) { + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.updateDrag.bindAsEventListener(this); + this.eventKeypress = this.keyPress.bindAsEventListener(this); + + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + Event.observe(document, "keypress", this.eventKeypress); + } + this.drags.push(draggable); + }, + + unregister: function(draggable) { + this.drags = this.drags.reject(function(d) { return d==draggable }); + if(this.drags.length == 0) { + Event.stopObserving(document, "mouseup", this.eventMouseUp); + Event.stopObserving(document, "mousemove", this.eventMouseMove); + Event.stopObserving(document, "keypress", this.eventKeypress); + } + }, + + activate: function(draggable) { + window.focus(); // allows keypress events if window isn't currently focused, fails for Safari + this.activeDraggable = draggable; + }, + + deactivate: function() { + this.activeDraggable = null; + }, + + updateDrag: function(event) { + if(!this.activeDraggable) return; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + // Mozilla-based browsers fire successive mousemove events with + // the same coordinates, prevent needless redrawing (moz bug?) + if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; + this._lastPointer = pointer; + this.activeDraggable.updateDrag(event, pointer); + }, + + endDrag: function(event) { + if(!this.activeDraggable) return; + this._lastPointer = null; + this.activeDraggable.endDrag(event); + this.activeDraggable = null; + }, + + keyPress: function(event) { + if(this.activeDraggable) + this.activeDraggable.keyPress(event); + }, + + addObserver: function(observer) { + this.observers.push(observer); + this._cacheObserverCallbacks(); + }, + + removeObserver: function(element) { // element instead of observer fixes mem leaks + this.observers = this.observers.reject( function(o) { return o.element==element }); + this._cacheObserverCallbacks(); + }, + + notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' + if(this[eventName+'Count'] > 0) + this.observers.each( function(o) { + if(o[eventName]) o[eventName](eventName, draggable, event); + }); + }, + + _cacheObserverCallbacks: function() { + ['onStart','onEnd','onDrag'].each( function(eventName) { + Draggables[eventName+'Count'] = Draggables.observers.select( + function(o) { return o[eventName]; } + ).length; + }); + } +} + +/*--------------------------------------------------------------------------*/ + +var Draggable = Class.create(); +Draggable.prototype = { + initialize: function(element) { + var options = Object.extend({ + handle: false, + starteffect: function(element) { + new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); + }, + reverteffect: function(element, top_offset, left_offset) { + var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; + element._revert = new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur}); + }, + endeffect: function(element) { + new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); + }, + zindex: 1000, + revert: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] } + }, arguments[1] || {}); + + this.element = $(element); + + if(options.handle && (typeof options.handle == 'string')) { + var h = Element.childrenWithClassName(this.element, options.handle, true); + if(h.length>0) this.handle = h[0]; + } + if(!this.handle) this.handle = $(options.handle); + if(!this.handle) this.handle = this.element; + + if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) + options.scroll = $(options.scroll); + + Element.makePositioned(this.element); // fix IE + + this.delta = this.currentDelta(); + this.options = options; + this.dragging = false; + + this.eventMouseDown = this.initDrag.bindAsEventListener(this); + Event.observe(this.handle, "mousedown", this.eventMouseDown); + + Draggables.register(this); + }, + + destroy: function() { + Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); + Draggables.unregister(this); + }, + + currentDelta: function() { + return([ + parseInt(Element.getStyle(this.element,'left') || '0'), + parseInt(Element.getStyle(this.element,'top') || '0')]); + }, + + initDrag: function(event) { + if(Event.isLeftClick(event)) { + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if(src.tagName && ( + src.tagName=='INPUT' || + src.tagName=='SELECT' || + src.tagName=='OPTION' || + src.tagName=='BUTTON' || + src.tagName=='TEXTAREA')) return; + + if(this.element._revert) { + this.element._revert.cancel(); + this.element._revert = null; + } + + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var pos = Position.cumulativeOffset(this.element); + this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); + + Draggables.activate(this); + Event.stop(event); + } + }, + + startDrag: function(event) { + this.dragging = true; + + if(this.options.zindex) { + this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); + this.element.style.zIndex = this.options.zindex; + } + + if(this.options.ghosting) { + this._clone = this.element.cloneNode(true); + Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + + if(this.options.scroll) { + if (this.options.scroll == window) { + var where = this._getWindowScroll(this.options.scroll); + this.originalScrollLeft = where.left; + this.originalScrollTop = where.top; + } else { + this.originalScrollLeft = this.options.scroll.scrollLeft; + this.originalScrollTop = this.options.scroll.scrollTop; + } + } + + Draggables.notify('onStart', this, event); + if(this.options.starteffect) this.options.starteffect(this.element); + }, + + updateDrag: function(event, pointer) { + if(!this.dragging) this.startDrag(event); + Position.prepare(); + Droppables.show(pointer, this.element); + Draggables.notify('onDrag', this, event); + this.draw(pointer); + if(this.options.change) this.options.change(this); + + if(this.options.scroll) { + this.stopScrolling(); + + var p; + if (this.options.scroll == window) { + with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } + } else { + p = Position.page(this.options.scroll); + p[0] += this.options.scroll.scrollLeft; + p[1] += this.options.scroll.scrollTop; + p.push(p[0]+this.options.scroll.offsetWidth); + p.push(p[1]+this.options.scroll.offsetHeight); + } + var speed = [0,0]; + if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); + if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); + if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); + if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); + this.startScrolling(speed); + } + + // fix AppleWebKit rendering + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + + Event.stop(event); + }, + + finishDrag: function(event, success) { + this.dragging = false; + + if(this.options.ghosting) { + Position.relativize(this.element); + Element.remove(this._clone); + this._clone = null; + } + + if(success) Droppables.fire(event, this.element); + Draggables.notify('onEnd', this, event); + + var revert = this.options.revert; + if(revert && typeof revert == 'function') revert = revert(this.element); + + var d = this.currentDelta(); + if(revert && this.options.reverteffect) { + this.options.reverteffect(this.element, + d[1]-this.delta[1], d[0]-this.delta[0]); + } else { + this.delta = d; + } + + if(this.options.zindex) + this.element.style.zIndex = this.originalZ; + + if(this.options.endeffect) + this.options.endeffect(this.element); + + Draggables.deactivate(this); + Droppables.reset(); + }, + + keyPress: function(event) { + if(event.keyCode!=Event.KEY_ESC) return; + this.finishDrag(event, false); + Event.stop(event); + }, + + endDrag: function(event) { + if(!this.dragging) return; + this.stopScrolling(); + this.finishDrag(event, true); + Event.stop(event); + }, + + draw: function(point) { + var pos = Position.cumulativeOffset(this.element); + var d = this.currentDelta(); + pos[0] -= d[0]; pos[1] -= d[1]; + + if(this.options.scroll && (this.options.scroll != window)) { + pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; + pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; + } + + var p = [0,1].map(function(i){ + return (point[i]-pos[i]-this.offset[i]) + }.bind(this)); + + if(this.options.snap) { + if(typeof this.options.snap == 'function') { + p = this.options.snap(p[0],p[1]); + } else { + if(this.options.snap instanceof Array) { + p = p.map( function(v, i) { + return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this)) + } else { + p = p.map( function(v) { + return Math.round(v/this.options.snap)*this.options.snap }.bind(this)) + } + }} + + var style = this.element.style; + if((!this.options.constraint) || (this.options.constraint=='horizontal')) + style.left = p[0] + "px"; + if((!this.options.constraint) || (this.options.constraint=='vertical')) + style.top = p[1] + "px"; + if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering + }, + + stopScrolling: function() { + if(this.scrollInterval) { + clearInterval(this.scrollInterval); + this.scrollInterval = null; + Draggables._lastScrollPointer = null; + } + }, + + startScrolling: function(speed) { + this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; + this.lastScrolled = new Date(); + this.scrollInterval = setInterval(this.scroll.bind(this), 10); + }, + + scroll: function() { + var current = new Date(); + var delta = current - this.lastScrolled; + this.lastScrolled = current; + if(this.options.scroll == window) { + with (this._getWindowScroll(this.options.scroll)) { + if (this.scrollSpeed[0] || this.scrollSpeed[1]) { + var d = delta / 1000; + this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); + } + } + } else { + this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; + this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; + } + + Position.prepare(); + Droppables.show(Draggables._lastPointer, this.element); + Draggables.notify('onDrag', this); + Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); + Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; + Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; + if (Draggables._lastScrollPointer[0] < 0) + Draggables._lastScrollPointer[0] = 0; + if (Draggables._lastScrollPointer[1] < 0) + Draggables._lastScrollPointer[1] = 0; + this.draw(Draggables._lastScrollPointer); + + if(this.options.change) this.options.change(this); + }, + + _getWindowScroll: function(w) { + var T, L, W, H; + with (w.document) { + if (w.document.documentElement && documentElement.scrollTop) { + T = documentElement.scrollTop; + L = documentElement.scrollLeft; + } else if (w.document.body) { + T = body.scrollTop; + L = body.scrollLeft; + } + if (w.innerWidth) { + W = w.innerWidth; + H = w.innerHeight; + } else if (w.document.documentElement && documentElement.clientWidth) { + W = documentElement.clientWidth; + H = documentElement.clientHeight; + } else { + W = body.offsetWidth; + H = body.offsetHeight + } + } + return { top: T, left: L, width: W, height: H }; + } +} + +/*--------------------------------------------------------------------------*/ + +var SortableObserver = Class.create(); +SortableObserver.prototype = { + initialize: function(element, observer) { + this.element = $(element); + this.observer = observer; + this.lastValue = Sortable.serialize(this.element); + }, + + onStart: function() { + this.lastValue = Sortable.serialize(this.element); + }, + + onEnd: function() { + Sortable.unmark(); + if(this.lastValue != Sortable.serialize(this.element)) + this.observer(this.element) + } +} + +var Sortable = { + sortables: {}, + + _findRootElement: function(element) { + while (element.tagName != "BODY") { + if(element.id && Sortable.sortables[element.id]) return element; + element = element.parentNode; + } + }, + + options: function(element) { + element = Sortable._findRootElement($(element)); + if(!element) return; + return Sortable.sortables[element.id]; + }, + + destroy: function(element){ + var s = Sortable.options(element); + + if(s) { + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + + delete Sortable.sortables[s.element.id]; + } + }, + + create: function(element) { + element = $(element); + var options = Object.extend({ + element: element, + tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + tree: false, + treeTag: 'ul', + overlap: 'vertical', // one of 'vertical', 'horizontal' + constraint: 'vertical', // one of 'vertical', 'horizontal', false + containment: element, // also takes array of elements (or id's); or false + handle: false, // or a CSS class + only: false, + hoverclass: null, + ghosting: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + format: /^[^_]*_(.*)$/, + onChange: Prototype.emptyFunction, + onUpdate: Prototype.emptyFunction + }, arguments[1] || {}); + + // clear any old sortable with same element + this.destroy(element); + + // build options for the draggables + var options_for_draggable = { + revert: true, + scroll: options.scroll, + scrollSpeed: options.scrollSpeed, + scrollSensitivity: options.scrollSensitivity, + ghosting: options.ghosting, + constraint: options.constraint, + handle: options.handle }; + + if(options.starteffect) + options_for_draggable.starteffect = options.starteffect; + + if(options.reverteffect) + options_for_draggable.reverteffect = options.reverteffect; + else + if(options.ghosting) options_for_draggable.reverteffect = function(element) { + element.style.top = 0; + element.style.left = 0; + }; + + if(options.endeffect) + options_for_draggable.endeffect = options.endeffect; + + if(options.zindex) + options_for_draggable.zindex = options.zindex; + + // build options for the droppables + var options_for_droppable = { + overlap: options.overlap, + containment: options.containment, + tree: options.tree, + hoverclass: options.hoverclass, + onHover: Sortable.onHover + //greedy: !options.dropOnEmpty + } + + var options_for_tree = { + onHover: Sortable.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass + } + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // drop on empty handling + if(options.dropOnEmpty || options.tree) { + Droppables.add(element, options_for_tree); + options.droppables.push(element); + } + + (this.findElements(element, options) || []).each( function(e) { + // handles are per-draggable + var handle = options.handle ? + Element.childrenWithClassName(e, options.handle)[0] : e; + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + if(options.tree) e.treeNode = element; + options.droppables.push(e); + }); + + if(options.tree) { + (Sortable.findTreeElements(element, options) || []).each( function(e) { + Droppables.add(e, options_for_tree); + e.treeNode = element; + options.droppables.push(e); + }); + } + + // keep reference + this.sortables[element.id] = options; + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.tag); + }, + + findTreeElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.treeTag); + }, + + onHover: function(element, dropon, overlap) { + if(Element.isParent(dropon, element)) return; + + if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { + return; + } else if(overlap>0.5) { + Sortable.mark(dropon, 'before'); + if(dropon.previousSibling != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } else { + Sortable.mark(dropon, 'after'); + var nextElement = dropon.nextSibling || null; + if(nextElement != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, nextElement); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } + }, + + onEmptyHover: function(element, dropon, overlap) { + var oldParentNode = element.parentNode; + var droponOptions = Sortable.options(dropon); + + if(!Element.isParent(dropon, element)) { + var index; + + var children = Sortable.findElements(dropon, {tag: droponOptions.tag}); + var child = null; + + if(children) { + var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); + + for (index = 0; index < children.length; index += 1) { + if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { + offset -= Element.offsetSize (children[index], droponOptions.overlap); + } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { + child = index + 1 < children.length ? children[index + 1] : null; + break; + } else { + child = children[index]; + break; + } + } + } + + dropon.insertBefore(element, child); + + Sortable.options(oldParentNode).onChange(element); + droponOptions.onChange(element); + } + }, + + unmark: function() { + if(Sortable._marker) Element.hide(Sortable._marker); + }, + + mark: function(dropon, position) { + // mark on ghosting only + var sortable = Sortable.options(dropon.parentNode); + if(sortable && !sortable.ghosting) return; + + if(!Sortable._marker) { + Sortable._marker = $('dropmarker') || document.createElement('DIV'); + Element.hide(Sortable._marker); + Element.addClassName(Sortable._marker, 'dropmarker'); + Sortable._marker.style.position = 'absolute'; + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = Position.cumulativeOffset(dropon); + Sortable._marker.style.left = offsets[0] + 'px'; + Sortable._marker.style.top = offsets[1] + 'px'; + + if(position=='after') + if(sortable.overlap == 'horizontal') + Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px'; + else + Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px'; + + Element.show(Sortable._marker); + }, + + _tree: function(element, options, parent) { + var children = Sortable.findElements(element, options) || []; + + for (var i = 0; i < children.length; ++i) { + var match = children[i].id.match(options.format); + + if (!match) continue; + + var child = { + id: encodeURIComponent(match ? match[1] : null), + element: element, + parent: parent, + children: new Array, + position: parent.children.length, + container: Sortable._findChildrenElement(children[i], options.treeTag.toUpperCase()) + } + + /* Get the element containing the children and recurse over it */ + if (child.container) + this._tree(child.container, options, child) + + parent.children.push (child); + } + + return parent; + }, + + /* Finds the first element of the given tag type within a parent element. + Used for finding the first LI[ST] within a L[IST]I[TEM].*/ + _findChildrenElement: function (element, containerTag) { + if (element && element.hasChildNodes) + for (var i = 0; i < element.childNodes.length; ++i) + if (element.childNodes[i].tagName == containerTag) + return element.childNodes[i]; + + return null; + }, + + tree: function(element) { + element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + treeTag: sortableOptions.treeTag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format + }, arguments[1] || {}); + + var root = { + id: null, + parent: null, + children: new Array, + container: element, + position: 0 + } + + return Sortable._tree (element, options, root); + }, + + /* Construct a [i] index for a particular node */ + _constructIndex: function(node) { + var index = ''; + do { + if (node.id) index = '[' + node.position + ']' + index; + } while ((node = node.parent) != null); + return index; + }, + + sequence: function(element) { + element = $(element); + var options = Object.extend(this.options(element), arguments[1] || {}); + + return $(this.findElements(element, options) || []).map( function(item) { + return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; + }); + }, + + setSequence: function(element, new_sequence) { + element = $(element); + var options = Object.extend(this.options(element), arguments[2] || {}); + + var nodeMap = {}; + this.findElements(element, options).each( function(n) { + if (n.id.match(options.format)) + nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; + n.parentNode.removeChild(n); + }); + + new_sequence.each(function(ident) { + var n = nodeMap[ident]; + if (n) { + n[1].appendChild(n[0]); + delete nodeMap[ident]; + } + }); + }, + + serialize: function(element) { + element = $(element); + var options = Object.extend(Sortable.options(element), arguments[1] || {}); + var name = encodeURIComponent( + (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); + + if (options.tree) { + return Sortable.tree(element, arguments[1]).children.map( function (item) { + return [name + Sortable._constructIndex(item) + "=" + + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); + }).flatten().join('&'); + } else { + return Sortable.sequence(element, arguments[1]).map( function(item) { + return name + "[]=" + encodeURIComponent(item); + }).join('&'); + } + } +} + +/* Returns true if child is contained within element */ +Element.isParent = function(child, element) { + if (!child.parentNode || child == element) return false; + + if (child.parentNode == element) return true; + + return Element.isParent(child.parentNode, element); +} + +Element.findChildren = function(element, only, recursive, tagName) { + if(!element.hasChildNodes()) return null; + tagName = tagName.toUpperCase(); + if(only) only = [only].flatten(); + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName.toUpperCase()==tagName && + (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) + elements.push(e); + if(recursive) { + var grandchildren = Element.findChildren(e, only, recursive, tagName); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : []); +} + +Element.offsetSize = function (element, type) { + if (type == 'vertical' || type == 'height') + return element.offsetHeight; + else + return element.offsetWidth; +} \ No newline at end of file diff --git a/vendor/rails/railties/html/javascripts/effects.js b/vendor/rails/railties/html/javascripts/effects.js new file mode 100644 index 00000000..92740050 --- /dev/null +++ b/vendor/rails/railties/html/javascripts/effects.js @@ -0,0 +1,958 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// See scriptaculous.js for full license. + +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + var color = '#'; + if(this.slice(0,4) == 'rgb(') { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if(this.slice(0,1) == '#') { + if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if(this.length==7) color = this.toLowerCase(); + } + } + return(color.length==7 ? color : (arguments[0] || this)); +} + +/*--------------------------------------------------------------------------*/ + +Element.collectTextNodes = function(element) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); + }).flatten().join(''); +} + +Element.collectTextNodesIgnoreClass = function(element, className) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? + Element.collectTextNodesIgnoreClass(node, className) : '')); + }).flatten().join(''); +} + +Element.setContentZoom = function(element, percent) { + element = $(element); + Element.setStyle(element, {fontSize: (percent/100) + 'em'}); + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); +} + +Element.getOpacity = function(element){ + var opacity; + if (opacity = Element.getStyle(element, 'opacity')) + return parseFloat(opacity); + if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/)) + if(opacity[1]) return parseFloat(opacity[1]) / 100; + return 1.0; +} + +Element.setOpacity = function(element, value){ + element= $(element); + if (value == 1){ + Element.setStyle(element, { opacity: + (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? + 0.999999 : null }); + if(/MSIE/.test(navigator.userAgent)) + Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')}); + } else { + if(value < 0.00001) value = 0; + Element.setStyle(element, {opacity: value}); + if(/MSIE/.test(navigator.userAgent)) + Element.setStyle(element, + { filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') + + 'alpha(opacity='+value*100+')' }); + } +} + +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; +} + +Element.childrenWithClassName = function(element, className, findFirst) { + var classNameRegExp = new RegExp("(^|\\s)" + className + "(\\s|$)"); + var results = $A($(element).getElementsByTagName('*'))[findFirst ? 'detect' : 'select']( function(c) { + return (c.className && c.className.match(classNameRegExp)); + }); + if(!results) results = []; + return results; +} + +Element.forceRerendering = function(element) { + try { + element = $(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { } +}; + +/*--------------------------------------------------------------------------*/ + +Array.prototype.call = function() { + var args = arguments; + this.each(function(f){ f.apply(this, args) }); +} + +/*--------------------------------------------------------------------------*/ + +var Effect = { + tagifyText: function(element) { + var tagifyStyle = 'position:relative'; + if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1'; + element = $(element); + $A(element.childNodes).each( function(child) { + if(child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + Builder.node('span',{style: tagifyStyle}, + character == ' ' ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if(((typeof element == 'object') || + (typeof element == 'function')) && + (element.length)) + elements = element; + else + elements = $(element).childNodes; + + var options = Object.extend({ + speed: 0.1, + delay: 0.0 + }, arguments[2] || {}); + var masterDelay = options.delay; + + $A(elements).each( function(element, index) { + new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); + }); + }, + PAIRS: { + 'slide': ['SlideDown','SlideUp'], + 'blind': ['BlindDown','BlindUp'], + 'appear': ['Appear','Fade'] + }, + toggle: function(element, effect) { + element = $(element); + effect = (effect || 'appear').toLowerCase(); + var options = Object.extend({ + queue: { position:'end', scope:(element.id || 'global'), limit: 1 } + }, arguments[2] || {}); + Effect[element.visible() ? + Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); + } +}; + +var Effect2 = Effect; // deprecated + +/* ------------- transitions ------------- */ + +Effect.Transitions = {} + +Effect.Transitions.linear = function(pos) { + return pos; +} +Effect.Transitions.sinoidal = function(pos) { + return (-Math.cos(pos*Math.PI)/2) + 0.5; +} +Effect.Transitions.reverse = function(pos) { + return 1-pos; +} +Effect.Transitions.flicker = function(pos) { + return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; +} +Effect.Transitions.wobble = function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; +} +Effect.Transitions.pulse = function(pos) { + return (Math.floor(pos*10) % 2 == 0 ? + (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10))); +} +Effect.Transitions.none = function(pos) { + return 0; +} +Effect.Transitions.full = function(pos) { + return 1; +} + +/* ------------- core effects ------------- */ + +Effect.ScopedQueue = Class.create(); +Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), { + initialize: function() { + this.effects = []; + this.interval = null; + }, + _each: function(iterator) { + this.effects._each(iterator); + }, + add: function(effect) { + var timestamp = new Date().getTime(); + + var position = (typeof effect.options.queue == 'string') ? + effect.options.queue : effect.options.queue.position; + + switch(position) { + case 'front': + // move unstarted effects after this effect + this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + }); + break; + case 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + + if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) + this.effects.push(effect); + + if(!this.interval) + this.interval = setInterval(this.loop.bind(this), 40); + }, + remove: function(effect) { + this.effects = this.effects.reject(function(e) { return e==effect }); + if(this.effects.length == 0) { + clearInterval(this.interval); + this.interval = null; + } + }, + loop: function() { + var timePos = new Date().getTime(); + this.effects.invoke('loop', timePos); + } +}); + +Effect.Queues = { + instances: $H(), + get: function(queueName) { + if(typeof queueName != 'string') return queueName; + + if(!this.instances[queueName]) + this.instances[queueName] = new Effect.ScopedQueue(); + + return this.instances[queueName]; + } +} +Effect.Queue = Effect.Queues.get('global'); + +Effect.DefaultOptions = { + transition: Effect.Transitions.sinoidal, + duration: 1.0, // seconds + fps: 25.0, // max. 25fps due to Effect.Queue implementation + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' +} + +Effect.Base = function() {}; +Effect.Base.prototype = { + position: null, + start: function(options) { + this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {}); + this.currentFrame = 0; + this.state = 'idle'; + this.startOn = this.options.delay*1000; + this.finishOn = this.startOn + (this.options.duration*1000); + this.event('beforeStart'); + if(!this.options.sync) + Effect.Queues.get(typeof this.options.queue == 'string' ? + 'global' : this.options.queue.scope).add(this); + }, + loop: function(timePos) { + if(timePos >= this.startOn) { + if(timePos >= this.finishOn) { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + if(this.finish) this.finish(); + this.event('afterFinish'); + return; + } + var pos = (timePos - this.startOn) / (this.finishOn - this.startOn); + var frame = Math.round(pos * this.options.fps * this.options.duration); + if(frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + render: function(pos) { + if(this.state == 'idle') { + this.state = 'running'; + this.event('beforeSetup'); + if(this.setup) this.setup(); + this.event('afterSetup'); + } + if(this.state == 'running') { + if(this.options.transition) pos = this.options.transition(pos); + pos *= (this.options.to-this.options.from); + pos += this.options.from; + this.position = pos; + this.event('beforeUpdate'); + if(this.update) this.update(pos); + this.event('afterUpdate'); + } + }, + cancel: function() { + if(!this.options.sync) + Effect.Queues.get(typeof this.options.queue == 'string' ? + 'global' : this.options.queue.scope).remove(this); + this.state = 'finished'; + }, + event: function(eventName) { + if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if(this.options[eventName]) this.options[eventName](this); + }, + inspect: function() { + return '#'; + } +} + +Effect.Parallel = Class.create(); +Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { + initialize: function(effects) { + this.effects = effects || []; + this.start(arguments[1]); + }, + update: function(position) { + this.effects.invoke('render', position); + }, + finish: function(position) { + this.effects.each( function(effect) { + effect.render(1.0); + effect.cancel(); + effect.event('beforeFinish'); + if(effect.finish) effect.finish(position); + effect.event('afterFinish'); + }); + } +}); + +Effect.Opacity = Class.create(); +Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + // make this work on IE on elements without 'layout' + if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout)) + this.element.setStyle({zoom: 1}); + var options = Object.extend({ + from: this.element.getOpacity() || 0.0, + to: 1.0 + }, arguments[1] || {}); + this.start(options); + }, + update: function(position) { + this.element.setOpacity(position); + } +}); + +Effect.Move = Class.create(); +Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + var options = Object.extend({ + x: 0, + y: 0, + mode: 'relative' + }, arguments[1] || {}); + this.start(options); + }, + setup: function() { + // Bug in Opera: Opera returns the "real" position of a static element or + // relative element that does not have top/left explicitly set. + // ==> Always set top and left for position relative elements in your stylesheets + // (to 0 if you do not need them) + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if(this.options.mode == 'absolute') { + // absolute movement, so we need to calc deltaX and deltaY + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } + }, + update: function(position) { + this.element.setStyle({ + left: this.options.x * position + this.originalLeft + 'px', + top: this.options.y * position + this.originalTop + 'px' + }); + } +}); + +// for backwards compatibility +Effect.MoveBy = function(element, toTop, toLeft) { + return new Effect.Move(element, + Object.extend({ x: toLeft, y: toTop }, arguments[3] || {})); +}; + +Effect.Scale = Class.create(); +Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { + initialize: function(element, percent) { + this.element = $(element) + var options = Object.extend({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or {} with provided values + scaleFrom: 100.0, + scaleTo: percent + }, arguments[2] || {}); + this.start(options); + }, + setup: function() { + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = this.element.getStyle('position'); + + this.originalStyle = {}; + ['top','left','width','height','fontSize'].each( function(k) { + this.originalStyle[k] = this.element.style[k]; + }.bind(this)); + + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + + var fontSize = this.element.getStyle('font-size') || '100%'; + ['em','px','%'].each( function(fontSizeType) { + if(fontSize.indexOf(fontSizeType)>0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = fontSizeType; + } + }.bind(this)); + + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + + this.dims = null; + if(this.options.scaleMode=='box') + this.dims = [this.element.offsetHeight, this.element.offsetWidth]; + if(/^content/.test(this.options.scaleMode)) + this.dims = [this.element.scrollHeight, this.element.scrollWidth]; + if(!this.dims) + this.dims = [this.options.scaleMode.originalHeight, + this.options.scaleMode.originalWidth]; + }, + update: function(position) { + var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if(this.options.scaleContent && this.fontSize) + this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); + }, + setDimensions: function(height, width) { + var d = {}; + if(this.options.scaleX) d.width = width + 'px'; + if(this.options.scaleY) d.height = height + 'px'; + if(this.options.scaleFromCenter) { + var topd = (height - this.dims[0])/2; + var leftd = (width - this.dims[1])/2; + if(this.elementPositioning == 'absolute') { + if(this.options.scaleY) d.top = this.originalTop-topd + 'px'; + if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; + } else { + if(this.options.scaleY) d.top = -topd + 'px'; + if(this.options.scaleX) d.left = -leftd + 'px'; + } + } + this.element.setStyle(d); + } +}); + +Effect.Highlight = Class.create(); +Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {}); + this.start(options); + }, + setup: function() { + // Prevent executing on elements not in the layout flow + if(this.element.getStyle('display')=='none') { this.cancel(); return; } + // Disable background image during the effect + this.oldStyle = { + backgroundImage: this.element.getStyle('background-image') }; + this.element.setStyle({backgroundImage: 'none'}); + if(!this.options.endcolor) + this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); + if(!this.options.restorecolor) + this.options.restorecolor = this.element.getStyle('background-color'); + // init color calculations + this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); + this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); + }, + update: function(position) { + this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ + return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) }); + }, + finish: function() { + this.element.setStyle(Object.extend(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); + } +}); + +Effect.ScrollTo = Class.create(); +Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + this.start(arguments[1] || {}); + }, + setup: function() { + Position.prepare(); + var offsets = Position.cumulativeOffset(this.element); + if(this.options.offset) offsets[1] += this.options.offset; + var max = window.innerHeight ? + window.height - window.innerHeight : + document.body.scrollHeight - + (document.documentElement.clientHeight ? + document.documentElement.clientHeight : document.body.clientHeight); + this.scrollStart = Position.deltaY; + this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart; + }, + update: function(position) { + Position.prepare(); + window.scrollTo(Position.deltaX, + this.scrollStart + (position*this.delta)); + } +}); + +/* ------------- combination effects ------------- */ + +Effect.Fade = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + var options = Object.extend({ + from: element.getOpacity() || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { + if(effect.options.to!=0) return; + effect.element.hide(); + effect.element.setStyle({opacity: oldOpacity}); + }}, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Appear = function(element) { + element = $(element); + var options = Object.extend({ + from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), + to: 1.0, + // force Safari to render floated elements properly + afterFinishInternal: function(effect) { + effect.element.forceRerendering(); + }, + beforeSetup: function(effect) { + effect.element.setOpacity(effect.options.from); + effect.element.show(); + }}, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Puff = function(element) { + element = $(element); + var oldStyle = { opacity: element.getInlineOpacity(), position: element.getStyle('position') }; + return new Effect.Parallel( + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) { + effect.effects[0].element.setStyle({position: 'absolute'}); }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide(); + effect.effects[0].element.setStyle(oldStyle); } + }, arguments[1] || {}) + ); +} + +Effect.BlindUp = function(element) { + element = $(element); + element.makeClipping(); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + } + }, arguments[1] || {}) + ); +} + +Effect.BlindDown = function(element) { + element = $(element); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, + Object.extend({ scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makeClipping(); + effect.element.setStyle({height: '0px'}); + effect.element.show(); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + } + }, arguments[1] || {}) + ); +} + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + return new Effect.Appear(element, { + duration: 0.4, + from: 0, + transition: Effect.Transitions.flicker, + afterFinishInternal: function(effect) { + new Effect.Scale(effect.element, 1, { + duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makePositioned(); + effect.element.makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + effect.element.undoPositioned(); + effect.element.setStyle({opacity: oldOpacity}); + } + }) + } + }); +} + +Effect.DropOut = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left'), + opacity: element.getInlineOpacity() }; + return new Effect.Parallel( + [ new Effect.Move(element, {x: 0, y: 100, sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + effect.effects[0].element.makePositioned(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide(); + effect.effects[0].element.undoPositioned(); + effect.effects[0].element.setStyle(oldStyle); + } + }, arguments[1] || {})); +} + +Effect.Shake = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + effect.element.undoPositioned(); + effect.element.setStyle(oldStyle); + }}) }}) }}) }}) }}) }}); +} + +Effect.SlideDown = function(element) { + element = $(element); + element.cleanWhitespace(); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = $(element.firstChild).getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.firstChild.makePositioned(); + if(window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping(); + effect.element.setStyle({height: '0px'}); + effect.element.show(); }, + afterUpdateInternal: function(effect) { + effect.element.firstChild.setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + // IE will crash if child is undoPositioned first + if(/MSIE/.test(navigator.userAgent)){ + effect.element.undoPositioned(); + effect.element.firstChild.undoPositioned(); + }else{ + effect.element.firstChild.undoPositioned(); + effect.element.undoPositioned(); + } + effect.element.firstChild.setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || {}) + ); +} + +Effect.SlideUp = function(element) { + element = $(element); + element.cleanWhitespace(); + var oldInnerBottom = $(element.firstChild).getStyle('bottom'); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + restoreAfterFinish: true, + beforeStartInternal: function(effect) { + effect.element.makePositioned(); + effect.element.firstChild.makePositioned(); + if(window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping(); + effect.element.show(); }, + afterUpdateInternal: function(effect) { + effect.element.firstChild.setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); }, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + effect.element.firstChild.undoPositioned(); + effect.element.undoPositioned(); + effect.element.setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || {}) + ); +} + +// Bug in opera makes the TD containing this element expand for a instance after finish +Effect.Squish = function(element) { + return new Effect.Scale(element, window.opera ? 1 : 0, + { restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makeClipping(effect.element); }, + afterFinishInternal: function(effect) { + effect.element.hide(effect.element); + effect.element.undoClipping(effect.element); } + }); +} + +Effect.Grow = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.full + }, arguments[1] || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var initialMoveX, initialMoveY; + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = dims.width; + initialMoveY = moveY = 0; + moveX = -dims.width; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = dims.height; + moveY = -dims.height; + break; + case 'bottom-right': + initialMoveX = dims.width; + initialMoveY = dims.height; + moveX = -dims.width; + moveY = -dims.height; + break; + case 'center': + initialMoveX = dims.width / 2; + initialMoveY = dims.height / 2; + moveX = -dims.width / 2; + moveY = -dims.height / 2; + break; + } + + return new Effect.Move(element, { + x: initialMoveX, + y: initialMoveY, + duration: 0.01, + beforeSetup: function(effect) { + effect.element.hide(); + effect.element.makeClipping(); + effect.element.makePositioned(); + }, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), + new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), + new Effect.Scale(effect.element, 100, { + scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, + sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) + ], Object.extend({ + beforeSetup: function(effect) { + effect.effects[0].element.setStyle({height: '0px'}); + effect.effects[0].element.show(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.undoClipping(); + effect.effects[0].element.undoPositioned(); + effect.effects[0].element.setStyle(oldStyle); + } + }, options) + ) + } + }); +} + +Effect.Shrink = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.none + }, arguments[1] || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = dims.width; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = dims.height; + break; + case 'bottom-right': + moveX = dims.width; + moveY = dims.height; + break; + case 'center': + moveX = dims.width / 2; + moveY = dims.height / 2; + break; + } + + return new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), + new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + effect.effects[0].element.makePositioned(); + effect.effects[0].element.makeClipping(); }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide(); + effect.effects[0].element.undoClipping(); + effect.effects[0].element.undoPositioned(); + effect.effects[0].element.setStyle(oldStyle); } + }, options) + ); +} + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || {}; + var oldOpacity = element.getInlineOpacity(); + var transition = options.transition || Effect.Transitions.sinoidal; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) }; + reverser.bind(transition); + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 3.0, from: 0, + afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } + }, options), {transition: reverser})); +} + +Effect.Fold = function(element) { + element = $(element); + var oldStyle = { + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height }; + Element.makeClipping(element); + return new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + effect.element.setStyle(oldStyle); + } }); + }}, arguments[1] || {})); +}; + +['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom', + 'collectTextNodes','collectTextNodesIgnoreClass','childrenWithClassName'].each( + function(f) { Element.Methods[f] = Element[f]; } +); + +Element.Methods.visualEffect = function(element, effect, options) { + s = effect.gsub(/_/, '-').camelize(); + effect_class = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[effect_class](element, options); + return $(element); +}; + +Element.addMethods(); \ No newline at end of file diff --git a/vendor/rails/railties/html/javascripts/prototype.js b/vendor/rails/railties/html/javascripts/prototype.js new file mode 100644 index 00000000..0caf9cd7 --- /dev/null +++ b/vendor/rails/railties/html/javascripts/prototype.js @@ -0,0 +1,2006 @@ +/* Prototype JavaScript framework, version 1.5.0_rc0 + * (c) 2005 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://prototype.conio.net/ + * +/*--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.5.0_rc0', + ScriptFragment: '(?:)((\n|\r|.)*?)(?:<\/script>)', + + emptyFunction: function() {}, + K: function(x) {return x} +} + +var Class = { + create: function() { + return function() { + this.initialize.apply(this, arguments); + } + } +} + +var Abstract = new Object(); + +Object.extend = function(destination, source) { + for (var property in source) { + destination[property] = source[property]; + } + return destination; +} + +Object.inspect = function(object) { + try { + if (object == undefined) return 'undefined'; + if (object == null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } +} + +Function.prototype.bind = function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } +} + +Function.prototype.bindAsEventListener = function(object) { + var __method = this; + return function(event) { + return __method.call(object, event || window.event); + } +} + +Object.extend(Number.prototype, { + toColorPart: function() { + var digits = this.toString(16); + if (this < 16) return '0' + digits; + return digits; + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + } +}); + +var Try = { + these: function() { + var returnValue; + + for (var i = 0; i < arguments.length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) {} + } + + return returnValue; + } +} + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create(); +PeriodicalExecuter.prototype = { + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.callback(); + } finally { + this.currentlyExecuting = false; + } + } + } +} +Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += (replacement(match) || '').toString(); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, + + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = count === undefined ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, + + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return this; + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = truncation === undefined ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : this; + }, + + strip: function() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + }, + + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + stripScripts: function() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + }, + + extractScripts: function() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + }, + + evalScripts: function() { + return this.extractScripts().map(function(script) { return eval(script) }); + }, + + escapeHTML: function() { + var div = document.createElement('div'); + var text = document.createTextNode(this); + div.appendChild(text); + return div.innerHTML; + }, + + unescapeHTML: function() { + var div = document.createElement('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? div.childNodes[0].nodeValue : ''; + }, + + toQueryParams: function() { + var pairs = this.match(/^\??(.*)$/)[1].split('&'); + return pairs.inject({}, function(params, pairString) { + var pair = pairString.split('='); + params[pair[0]] = pair[1]; + return params; + }); + }, + + toArray: function() { + return this.split(''); + }, + + camelize: function() { + var oStringList = this.split('-'); + if (oStringList.length == 1) return oStringList[0]; + + var camelizedString = this.indexOf('-') == 0 + ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) + : oStringList[0]; + + for (var i = 1, len = oStringList.length; i < len; i++) { + var s = oStringList[i]; + camelizedString += s.charAt(0).toUpperCase() + s.substring(1); + } + + return camelizedString; + }, + + inspect: function() { + return "'" + this.replace(/\\/g, '\\\\').replace(/'/g, '\\\'') + "'"; + } +}); + +String.prototype.gsub.prepareReplacement = function(replacement) { + if (typeof replacement == 'function') return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +} + +String.prototype.parseQuery = String.prototype.toQueryParams; + +var Template = Class.create(); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; +Template.prototype = { + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + return this.template.gsub(this.pattern, function(match) { + var before = match[1]; + if (before == '\\') return match[2]; + return before + (object[match[3]] || '').toString(); + }); + } +} + +var $break = new Object(); +var $continue = new Object(); + +var Enumerable = { + each: function(iterator) { + var index = 0; + try { + this._each(function(value) { + try { + iterator(value, index++); + } catch (e) { + if (e != $continue) throw e; + } + }); + } catch (e) { + if (e != $break) throw e; + } + }, + + all: function(iterator) { + var result = true; + this.each(function(value, index) { + result = result && !!(iterator || Prototype.K)(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator) { + var result = true; + this.each(function(value, index) { + if (result = !!(iterator || Prototype.K)(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator) { + var results = []; + this.each(function(value, index) { + results.push(iterator(value, index)); + }); + return results; + }, + + detect: function (iterator) { + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator) { + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(pattern, iterator) { + var results = []; + this.each(function(value, index) { + var stringValue = value.toString(); + if (stringValue.match(pattern)) + results.push((iterator || Prototype.K)(value, index)); + }) + return results; + }, + + include: function(object) { + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inject: function(memo, iterator) { + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.collect(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator) { + var trues = [], falses = []; + this.each(function(value, index) { + ((iterator || Prototype.K)(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value, index) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator) { + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator) { + return this.collect(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.collect(Prototype.K); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (typeof args.last() == 'function') + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + }, + + inspect: function() { + return '#'; + } +} + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray +}); +var $A = Array.from = function(iterable) { + if (!iterable) return []; + if (iterable.toArray) { + return iterable.toArray(); + } else { + var results = []; + for (var i = 0; i < iterable.length; i++) + results.push(iterable[i]); + return results; + } +} + +Object.extend(Array.prototype, Enumerable); + +if (!Array.prototype._reverse) + Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0; i < this.length; i++) + iterator(this[i]); + }, + + clear: function() { + this.length = 0; + return this; + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != undefined || value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(value && value.constructor == Array ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + indexOf: function(object) { + for (var i = 0; i < this.length; i++) + if (this[i] == object) return i; + return -1; + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } +}); +var Hash = { + _each: function(iterator) { + for (var key in this) { + var value = this[key]; + if (typeof value == 'function') continue; + + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + merge: function(hash) { + return $H(hash).inject($H(this), function(mergedHash, pair) { + mergedHash[pair.key] = pair.value; + return mergedHash; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + return pair.map(encodeURIComponent).join('='); + }).join('&'); + }, + + inspect: function() { + return '#'; + } +} + +function $H(object) { + var hash = Object.extend({}, object || {}); + Object.extend(hash, Enumerable); + Object.extend(hash, Hash); + return hash; +} +ObjectRange = Class.create(); +Object.extend(ObjectRange.prototype, Enumerable); +Object.extend(ObjectRange.prototype, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + do { + iterator(value); + value = value.succ(); + } while (this.include(value)); + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +} + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new XMLHttpRequest()}, + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')} + ) || false; + }, + + activeRequestCount: 0 +} + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responderToAdd) { + if (!this.include(responderToAdd)) + this.responders.push(responderToAdd); + }, + + unregister: function(responderToRemove) { + this.responders = this.responders.without(responderToRemove); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (responder[callback] && typeof responder[callback] == 'function') { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) {} + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { + Ajax.activeRequestCount++; + }, + + onComplete: function() { + Ajax.activeRequestCount--; + } +}); + +Ajax.Base = function() {}; +Ajax.Base.prototype = { + setOptions: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + parameters: '' + } + Object.extend(this.options, options || {}); + }, + + responseIsSuccess: function() { + return this.transport.status == undefined + || this.transport.status == 0 + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + responseIsFailure: function() { + return !this.responseIsSuccess(); + } +} + +Ajax.Request = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + initialize: function(url, options) { + this.transport = Ajax.getTransport(); + this.setOptions(options); + this.request(url); + }, + + request: function(url) { + var parameters = this.options.parameters || ''; + if (parameters.length > 0) parameters += '&_='; + + try { + this.url = url; + if (this.options.method == 'get' && parameters.length > 0) + this.url += (this.url.match(/\?/) ? '&' : '?') + parameters; + + Ajax.Responders.dispatch('onCreate', this, this.transport); + + this.transport.open(this.options.method, this.url, + this.options.asynchronous); + + if (this.options.asynchronous) { + this.transport.onreadystatechange = this.onStateChange.bind(this); + setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); + } + + this.setRequestHeaders(); + + var body = this.options.postBody ? this.options.postBody : parameters; + this.transport.send(this.options.method == 'post' ? body : null); + + } catch (e) { + this.dispatchException(e); + } + }, + + setRequestHeaders: function() { + var requestHeaders = + ['X-Requested-With', 'XMLHttpRequest', + 'X-Prototype-Version', Prototype.Version, + 'Accept', 'text/javascript, text/html, application/xml, text/xml, */*']; + + if (this.options.method == 'post') { + requestHeaders.push('Content-type', this.options.contentType); + + /* Force "Connection: close" for Mozilla browsers to work around + * a bug where XMLHttpReqeuest sends an incorrect Content-length + * header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType) + requestHeaders.push('Connection', 'close'); + } + + if (this.options.requestHeaders) + requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); + + for (var i = 0; i < requestHeaders.length; i += 2) + this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState != 1) + this.respondToReadyState(this.transport.readyState); + }, + + header: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) {} + }, + + evalJSON: function() { + try { + return eval('(' + this.header('X-JSON') + ')'); + } catch (e) {} + }, + + evalResponse: function() { + try { + return eval(this.transport.responseText); + } catch (e) { + this.dispatchException(e); + } + }, + + respondToReadyState: function(readyState) { + var event = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (event == 'Complete') { + try { + (this.options['on' + this.transport.status] + || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(transport, json); + } catch (e) { + this.dispatchException(e); + } + + if ((this.header('Content-type') || '').match(/^text\/javascript/i)) + this.evalResponse(); + } + + try { + (this.options['on' + event] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + event, this, transport, json); + } catch (e) { + this.dispatchException(e); + } + + /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ + if (event == 'Complete') + this.transport.onreadystatechange = Prototype.emptyFunction; + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Updater = Class.create(); + +Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { + initialize: function(container, url, options) { + this.containers = { + success: container.success ? $(container.success) : $(container), + failure: container.failure ? $(container.failure) : + (container.success ? null : $(container)) + } + + this.transport = Ajax.getTransport(); + this.setOptions(options); + + var onComplete = this.options.onComplete || Prototype.emptyFunction; + this.options.onComplete = (function(transport, object) { + this.updateContent(); + onComplete(transport, object); + }).bind(this); + + this.request(url); + }, + + updateContent: function() { + var receiver = this.responseIsSuccess() ? + this.containers.success : this.containers.failure; + var response = this.transport.responseText; + + if (!this.options.evalScripts) + response = response.stripScripts(); + + if (receiver) { + if (this.options.insertion) { + new this.options.insertion(receiver, response); + } else { + Element.update(receiver, response); + } + } + + if (this.responseIsSuccess()) { + if (this.onComplete) + setTimeout(this.onComplete.bind(this), 10); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(); +Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { + initialize: function(container, url, options) { + this.setOptions(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = {}; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(request) { + if (this.options.decay) { + this.decay = (request.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = request.responseText; + } + this.timer = setTimeout(this.onTimerEvent.bind(this), + this.decay * this.frequency * 1000); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +function $() { + var results = [], element; + for (var i = 0; i < arguments.length; i++) { + element = arguments[i]; + if (typeof element == 'string') + element = document.getElementById(element); + results.push(Element.extend(element)); + } + return results.length < 2 ? results[0] : results; +} + +document.getElementsByClassName = function(className, parentElement) { + var children = ($(parentElement) || document.body).getElementsByTagName('*'); + return $A(children).inject([], function(elements, child) { + if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) + elements.push(Element.extend(child)); + return elements; + }); +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Element) + var Element = new Object(); + +Element.extend = function(element) { + if (!element) return; + if (_nativeExtensions) return element; + + if (!element._extended && element.tagName && element != window) { + var methods = Element.Methods, cache = Element.extend.cache; + for (property in methods) { + var value = methods[property]; + if (typeof value == 'function') + element[property] = cache.findOrStore(value); + } + } + + element._extended = true; + return element; +} + +Element.extend.cache = { + findOrStore: function(value) { + return this[value] = this[value] || function() { + return value.apply(null, [this].concat($A(arguments))); + } + } +} + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + Element[Element.visible(element) ? 'hide' : 'show'](element); + } + }, + + hide: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = 'none'; + } + }, + + show: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = ''; + } + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + }, + + update: function(element, html) { + $(element).innerHTML = html.stripScripts(); + setTimeout(function() {html.evalScripts()}, 10); + }, + + replace: function(element, html) { + element = $(element); + if (element.outerHTML) { + element.outerHTML = html.stripScripts(); + } else { + var range = element.ownerDocument.createRange(); + range.selectNodeContents(element); + element.parentNode.replaceChild( + range.createContextualFragment(html.stripScripts()), element); + } + setTimeout(function() {html.evalScripts()}, 10); + }, + + getHeight: function(element) { + element = $(element); + return element.offsetHeight; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).include(className); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).add(className); + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).remove(className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + for (var i = 0; i < element.childNodes.length; i++) { + var node = element.childNodes[i]; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + Element.remove(node); + } + }, + + empty: function(element) { + return $(element).innerHTML.match(/^\s*$/); + }, + + childOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + while (element = element.parentNode) + if (element == ancestor) return true; + return false; + }, + + scrollTo: function(element) { + element = $(element); + var x = element.x ? element.x : element.offsetLeft, + y = element.y ? element.y : element.offsetTop; + window.scrollTo(x, y); + }, + + getStyle: function(element, style) { + element = $(element); + var value = element.style[style.camelize()]; + if (!value) { + if (document.defaultView && document.defaultView.getComputedStyle) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css.getPropertyValue(style) : null; + } else if (element.currentStyle) { + value = element.currentStyle[style.camelize()]; + } + } + + if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) + if (Element.getStyle(element, 'position') == 'static') value = 'auto'; + + return value == 'auto' ? null : value; + }, + + setStyle: function(element, style) { + element = $(element); + for (var name in style) + element.style[name.camelize()] = style[name]; + }, + + getDimensions: function(element) { + element = $(element); + if (Element.getStyle(element, 'display') != 'none') + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = ''; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = 'none'; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (window.opera) { + element.style.top = 0; + element.style.left = 0; + } + } + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return; + element._overflow = element.style.overflow; + if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') + element.style.overflow = 'hidden'; + }, + + undoClipping: function(element) { + element = $(element); + if (element._overflow) return; + element.style.overflow = element._overflow; + element._overflow = undefined; + } +} + +Object.extend(Element, Element.Methods); + +var _nativeExtensions = false; + +if(!HTMLElement && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + var HTMLElement = {} + HTMLElement.prototype = document.createElement('div').__proto__; +} + +Element.addMethods = function(methods) { + Object.extend(Element.Methods, methods || {}); + + if(typeof HTMLElement != 'undefined') { + var methods = Element.Methods, cache = Element.extend.cache; + for (property in methods) { + var value = methods[property]; + if (typeof value == 'function') + HTMLElement.prototype[property] = cache.findOrStore(value); + } + _nativeExtensions = true; + } +} + +Element.addMethods(); + +var Toggle = new Object(); +Toggle.display = Element.toggle; + +/*--------------------------------------------------------------------------*/ + +Abstract.Insertion = function(adjacency) { + this.adjacency = adjacency; +} + +Abstract.Insertion.prototype = { + initialize: function(element, content) { + this.element = $(element); + this.content = content.stripScripts(); + + if (this.adjacency && this.element.insertAdjacentHTML) { + try { + this.element.insertAdjacentHTML(this.adjacency, this.content); + } catch (e) { + var tagName = this.element.tagName.toLowerCase(); + if (tagName == 'tbody' || tagName == 'tr') { + this.insertContent(this.contentFromAnonymousTable()); + } else { + throw e; + } + } + } else { + this.range = this.element.ownerDocument.createRange(); + if (this.initializeRange) this.initializeRange(); + this.insertContent([this.range.createContextualFragment(this.content)]); + } + + setTimeout(function() {content.evalScripts()}, 10); + }, + + contentFromAnonymousTable: function() { + var div = document.createElement('div'); + div.innerHTML = '' + this.content + '
      '; + return $A(div.childNodes[0].childNodes[0].childNodes); + } +} + +var Insertion = new Object(); + +Insertion.Before = Class.create(); +Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { + initializeRange: function() { + this.range.setStartBefore(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, this.element); + }).bind(this)); + } +}); + +Insertion.Top = Class.create(); +Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(true); + }, + + insertContent: function(fragments) { + fragments.reverse(false).each((function(fragment) { + this.element.insertBefore(fragment, this.element.firstChild); + }).bind(this)); + } +}); + +Insertion.Bottom = Class.create(); +Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.appendChild(fragment); + }).bind(this)); + } +}); + +Insertion.After = Class.create(); +Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { + initializeRange: function() { + this.range.setStartAfter(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, + this.element.nextSibling); + }).bind(this)); + } +}); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, + + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, + + set: function(className) { + this.element.className = className; + }, + + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set(this.toArray().concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set(this.select(function(className) { + return className != classNameToRemove; + }).join(' ')); + }, + + toString: function() { + return this.toArray().join(' '); + } +} + +Object.extend(Element.ClassNames.prototype, Enumerable); +var Selector = Class.create(); +Selector.prototype = { + initialize: function(expression) { + this.params = {classNames: []}; + this.expression = expression.toString().strip(); + this.parseExpression(); + this.compileMatcher(); + }, + + parseExpression: function() { + function abort(message) { throw 'Parse error in selector: ' + message; } + + if (this.expression == '') abort('empty expression'); + + var params = this.params, expr = this.expression, match, modifier, clause, rest; + while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) { + params.attributes = params.attributes || []; + params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''}); + expr = match[1]; + } + + if (expr == '*') return this.params.wildcard = true; + + while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) { + modifier = match[1], clause = match[2], rest = match[3]; + switch (modifier) { + case '#': params.id = clause; break; + case '.': params.classNames.push(clause); break; + case '': + case undefined: params.tagName = clause.toUpperCase(); break; + default: abort(expr.inspect()); + } + expr = rest; + } + + if (expr.length > 0) abort(expr.inspect()); + }, + + buildMatchExpression: function() { + var params = this.params, conditions = [], clause; + + if (params.wildcard) + conditions.push('true'); + if (clause = params.id) + conditions.push('element.id == ' + clause.inspect()); + if (clause = params.tagName) + conditions.push('element.tagName.toUpperCase() == ' + clause.inspect()); + if ((clause = params.classNames).length > 0) + for (var i = 0; i < clause.length; i++) + conditions.push('Element.hasClassName(element, ' + clause[i].inspect() + ')'); + if (clause = params.attributes) { + clause.each(function(attribute) { + var value = 'element.getAttribute(' + attribute.name.inspect() + ')'; + var splitValueBy = function(delimiter) { + return value + ' && ' + value + '.split(' + delimiter.inspect() + ')'; + } + + switch (attribute.operator) { + case '=': conditions.push(value + ' == ' + attribute.value.inspect()); break; + case '~=': conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break; + case '|=': conditions.push( + splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect() + ); break; + case '!=': conditions.push(value + ' != ' + attribute.value.inspect()); break; + case '': + case undefined: conditions.push(value + ' != null'); break; + default: throw 'Unknown operator ' + attribute.operator + ' in selector'; + } + }); + } + + return conditions.join(' && '); + }, + + compileMatcher: function() { + this.match = new Function('element', 'if (!element.tagName) return false; \ + return ' + this.buildMatchExpression()); + }, + + findElements: function(scope) { + var element; + + if (element = $(this.params.id)) + if (this.match(element)) + if (!scope || Element.childOf(element, scope)) + return [element]; + + scope = (scope || document).getElementsByTagName(this.params.tagName || '*'); + + var results = []; + for (var i = 0; i < scope.length; i++) + if (this.match(element = scope[i])) + results.push(Element.extend(element)); + + return results; + }, + + toString: function() { + return this.expression; + } +} + +function $$() { + return $A(arguments).map(function(expression) { + return expression.strip().split(/\s+/).inject([null], function(results, expr) { + var selector = new Selector(expr); + return results.map(selector.findElements.bind(selector)).flatten(); + }); + }).flatten(); +} +var Field = { + clear: function() { + for (var i = 0; i < arguments.length; i++) + $(arguments[i]).value = ''; + }, + + focus: function(element) { + $(element).focus(); + }, + + present: function() { + for (var i = 0; i < arguments.length; i++) + if ($(arguments[i]).value == '') return false; + return true; + }, + + select: function(element) { + $(element).select(); + }, + + activate: function(element) { + element = $(element); + element.focus(); + if (element.select) + element.select(); + } +} + +/*--------------------------------------------------------------------------*/ + +var Form = { + serialize: function(form) { + var elements = Form.getElements($(form)); + var queryComponents = new Array(); + + for (var i = 0; i < elements.length; i++) { + var queryComponent = Form.Element.serialize(elements[i]); + if (queryComponent) + queryComponents.push(queryComponent); + } + + return queryComponents.join('&'); + }, + + getElements: function(form) { + form = $(form); + var elements = new Array(); + + for (var tagName in Form.Element.Serializers) { + var tagElements = form.getElementsByTagName(tagName); + for (var j = 0; j < tagElements.length; j++) + elements.push(tagElements[j]); + } + return elements; + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) + return inputs; + + var matchingInputs = new Array(); + for (var i = 0; i < inputs.length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || + (name && input.name != name)) + continue; + matchingInputs.push(input); + } + + return matchingInputs; + }, + + disable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.blur(); + element.disabled = 'true'; + } + }, + + enable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.disabled = ''; + } + }, + + findFirstElement: function(form) { + return Form.getElements(form).find(function(element) { + return element.type != 'hidden' && !element.disabled && + ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + Field.activate(Form.findFirstElement(form)); + }, + + reset: function(form) { + $(form).reset(); + } +} + +Form.Element = { + serialize: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) { + var key = encodeURIComponent(parameter[0]); + if (key.length == 0) return; + + if (parameter[1].constructor != Array) + parameter[1] = [parameter[1]]; + + return parameter[1].map(function(value) { + return key + '=' + encodeURIComponent(value); + }).join('&'); + } + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) + return parameter[1]; + } +} + +Form.Element.Serializers = { + input: function(element) { + switch (element.type.toLowerCase()) { + case 'submit': + case 'hidden': + case 'password': + case 'text': + return Form.Element.Serializers.textarea(element); + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element); + } + return false; + }, + + inputSelector: function(element) { + if (element.checked) + return [element.name, element.value]; + }, + + textarea: function(element) { + return [element.name, element.value]; + }, + + select: function(element) { + return Form.Element.Serializers[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + + selectOne: function(element) { + var value = '', opt, index = element.selectedIndex; + if (index >= 0) { + opt = element.options[index]; + value = opt.value || opt.text; + } + return [element.name, value]; + }, + + selectMany: function(element) { + var value = []; + for (var i = 0; i < element.length; i++) { + var opt = element.options[i]; + if (opt.selected) + value.push(opt.value || opt.text); + } + return [element.name, value]; + } +} + +/*--------------------------------------------------------------------------*/ + +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = function() {} +Abstract.TimedObserver.prototype = { + initialize: function(element, frequency, callback) { + this.frequency = frequency; + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + } +} + +Form.Element.Observer = Class.create(); +Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(); +Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = function() {} +Abstract.EventObserver.prototype = { + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + var elements = Form.getElements(this.element); + for (var i = 0; i < elements.length; i++) + this.registerCallback(elements[i]); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + case 'password': + case 'text': + case 'textarea': + case 'select-one': + case 'select-multiple': + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +} + +Form.Element.EventObserver = Class.create(); +Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(); +Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) { + var Event = new Object(); +} + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + + element: function(event) { + return event.target || event.srcElement; + }, + + isLeftClick: function(event) { + return (((event.which) && (event.which == 1)) || + ((event.button) && (event.button == 1))); + }, + + pointerX: function(event) { + return event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)); + }, + + pointerY: function(event) { + return event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)); + }, + + stop: function(event) { + if (event.preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } else { + event.returnValue = false; + event.cancelBubble = true; + } + }, + + // find the first node with the given tagName, starting from the + // node the event was triggered on; traverses the DOM upwards + findElement: function(event, tagName) { + var element = Event.element(event); + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))) + element = element.parentNode; + return element; + }, + + observers: false, + + _observeAndCache: function(element, name, observer, useCapture) { + if (!this.observers) this.observers = []; + if (element.addEventListener) { + this.observers.push([element, name, observer, useCapture]); + element.addEventListener(name, observer, useCapture); + } else if (element.attachEvent) { + this.observers.push([element, name, observer, useCapture]); + element.attachEvent('on' + name, observer); + } + }, + + unloadCache: function() { + if (!Event.observers) return; + for (var i = 0; i < Event.observers.length; i++) { + Event.stopObserving.apply(this, Event.observers[i]); + Event.observers[i][0] = null; + } + Event.observers = false; + }, + + observe: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.attachEvent)) + name = 'keydown'; + + this._observeAndCache(element, name, observer, useCapture); + }, + + stopObserving: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.detachEvent)) + name = 'keydown'; + + if (element.removeEventListener) { + element.removeEventListener(name, observer, useCapture); + } else if (element.detachEvent) { + element.detachEvent('on' + name, observer); + } + } +}); + +/* prevent memory leaks in IE */ +if (navigator.appVersion.match(/\bMSIE\b/)) + Event.observe(window, 'unload', Event.unloadCache, false); +var Position = { + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + // must be called before calling withinIncludingScrolloffset, every time the + // page is scrolled + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + realOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return [valueL, valueT]; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return [valueL, valueT]; + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return [valueL, valueT]; + }, + + offsetParent: function(element) { + if (element.offsetParent) return element.offsetParent; + if (element == document.body) return element; + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return element; + + return document.body; + }, + + // caches x/y coordinate pair to use with overlap + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = this.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = this.realOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = this.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + // within must be called directly before + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + clone: function(source, target) { + source = $(source); + target = $(target); + target.style.position = 'absolute'; + var offsets = this.cumulativeOffset(source); + target.style.top = offsets[1] + 'px'; + target.style.left = offsets[0] + 'px'; + target.style.width = source.offsetWidth + 'px'; + target.style.height = source.offsetHeight + 'px'; + }, + + page: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent==document.body) + if (Element.getStyle(element,'position')=='absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } while (element = element.parentNode); + + return [valueL, valueT]; + }, + + clone: function(source, target) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || {}) + + // find page position of source + source = $(source); + var p = Position.page(source); + + // find coordinate system to use + target = $(target); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(target,'position') == 'absolute') { + parent = Position.offsetParent(target); + delta = Position.page(parent); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if(options.setWidth) target.style.width = source.offsetWidth + 'px'; + if(options.setHeight) target.style.height = source.offsetHeight + 'px'; + }, + + absolutize: function(element) { + element = $(element); + if (element.style.position == 'absolute') return; + Position.prepare(); + + var offsets = Position.positionedOffset(element); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px';; + element.style.left = left + 'px';; + element.style.width = width + 'px';; + element.style.height = height + 'px';; + }, + + relativize: function(element) { + element = $(element); + if (element.style.position == 'relative') return; + Position.prepare(); + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + } +} + +// Safari returns margins on body which is incorrect if the child is absolutely +// positioned. For performance reasons, redefine Position.cumulativeOffset for +// KHTML/WebKit only. +if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + Position.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return [valueL, valueT]; + } +} \ No newline at end of file diff --git a/vendor/rails/railties/html/robots.txt b/vendor/rails/railties/html/robots.txt new file mode 100644 index 00000000..4ab9e89f --- /dev/null +++ b/vendor/rails/railties/html/robots.txt @@ -0,0 +1 @@ +# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file \ No newline at end of file diff --git a/vendor/rails/railties/lib/binding_of_caller.rb b/vendor/rails/railties/lib/binding_of_caller.rb new file mode 100644 index 00000000..c1f2cc7b --- /dev/null +++ b/vendor/rails/railties/lib/binding_of_caller.rb @@ -0,0 +1,85 @@ +begin + require 'simplecc' +rescue LoadError + # to satisfy rdoc + class Continuation #:nodoc: + end + def Continuation.create(*args, &block) # :nodoc: + cc = nil; result = callcc {|c| cc = c; block.call(cc) if block and args.empty?} + result ||= args + return *[cc, *result] + end +end + +class Binding; end # for RDoc +# This method returns the binding of the method that called your +# method. It will raise an Exception when you're not inside a method. +# +# It's used like this: +# def inc_counter(amount = 1) +# Binding.of_caller do |binding| +# # Create a lambda that will increase the variable 'counter' +# # in the caller of this method when called. +# inc = eval("lambda { |arg| counter += arg }", binding) +# # We can refer to amount from inside this block safely. +# inc.call(amount) +# end +# # No other statements can go here. Put them inside the block. +# end +# counter = 0 +# 2.times { inc_counter } +# counter # => 2 +# +# Binding.of_caller must be the last statement in the method. +# This means that you will have to put everything you want to +# do after the call to Binding.of_caller into the block of it. +# This should be no problem however, because Ruby has closures. +# If you don't do this an Exception will be raised. Because of +# the way that Binding.of_caller is implemented it has to be +# done this way. +def Binding.of_caller(&block) + old_critical = Thread.critical + Thread.critical = true + count = 0 + cc, result, error, extra_data = Continuation.create(nil, nil) + error.call if error + + tracer = lambda do |*args| + type, context, extra_data = args[0], args[4], args + if type == "return" + count += 1 + # First this method and then calling one will return -- + # the trace event of the second event gets the context + # of the method which called the method that called this + # method. + if count == 2 + # It would be nice if we could restore the trace_func + # that was set before we swapped in our own one, but + # this is impossible without overloading set_trace_func + # in current Ruby. + set_trace_func(nil) + cc.call(eval("binding", context), nil, extra_data) + end + elsif type == "line" then + nil + elsif type == "c-return" and extra_data[3] == :set_trace_func then + nil + else + set_trace_func(nil) + error_msg = "Binding.of_caller used in non-method context or " + + "trailing statements of method using it aren't in the block." + cc.call(nil, lambda { raise(ArgumentError, error_msg) }, nil) + end + end + + unless result + set_trace_func(tracer) + return nil + else + Thread.critical = old_critical + case block.arity + when 1 then yield(result) + else yield(result, extra_data) + end + end +end diff --git a/vendor/rails/railties/lib/breakpoint.rb b/vendor/rails/railties/lib/breakpoint.rb new file mode 100644 index 00000000..327e43e8 --- /dev/null +++ b/vendor/rails/railties/lib/breakpoint.rb @@ -0,0 +1,523 @@ +# The Breakpoint library provides the convenience of +# being able to inspect and modify state, diagnose +# bugs all via IRB by simply setting breakpoints in +# your applications by the call of a method. +# +# This library was written and is supported by me, +# Florian Gross. I can be reached at flgr@ccan.de +# and enjoy getting feedback about my libraries. +# +# The whole library (including breakpoint_client.rb +# and binding_of_caller.rb) is licensed under the +# same license that Ruby uses. (Which is currently +# either the GNU General Public License or a custom +# one that allows for commercial usage.) If you for +# some good reason need to use this under another +# license please contact me. + +require 'irb' +require 'binding_of_caller' +require 'drb' +require 'drb/acl' + +module Breakpoint + id = %q$Id: breakpoint.rb 92 2005-02-04 22:35:53Z flgr $ + Version = id.split(" ")[2].to_i + + extend self + + # This will pop up an interactive ruby session at a + # pre-defined break point in a Ruby application. In + # this session you can examine the environment of + # the break point. + # + # You can get a list of variables in the context using + # local_variables via +local_variables+. You can then + # examine their values by typing their names. + # + # You can have a look at the call stack via +caller+. + # + # The source code around the location where the breakpoint + # was executed can be examined via +source_lines+. Its + # argument specifies how much lines of context to display. + # The default amount of context is 5 lines. Note that + # the call to +source_lines+ can raise an exception when + # it isn't able to read in the source code. + # + # breakpoints can also return a value. They will execute + # a supplied block for getting a default return value. + # A custom value can be returned from the session by doing + # +throw(:debug_return, value)+. + # + # You can also give names to break points which will be + # used in the message that is displayed upon execution + # of them. + # + # Here's a sample of how breakpoints should be placed: + # + # class Person + # def initialize(name, age) + # @name, @age = name, age + # breakpoint("Person#initialize") + # end + # + # attr_reader :age + # def name + # breakpoint("Person#name") { @name } + # end + # end + # + # person = Person.new("Random Person", 23) + # puts "Name: #{person.name}" + # + # And here is a sample debug session: + # + # Executing break point "Person#initialize" at file.rb:4 in `initialize' + # irb(#):001:0> local_variables + # => ["name", "age", "_", "__"] + # irb(#):002:0> [name, age] + # => ["Random Person", 23] + # irb(#):003:0> [@name, @age] + # => ["Random Person", 23] + # irb(#):004:0> self + # => # + # irb(#):005:0> @age += 1; self + # => # + # irb(#):006:0> exit + # Executing break point "Person#name" at file.rb:9 in `name' + # irb(#):001:0> throw(:debug_return, "Overriden name") + # Name: Overriden name + # + # Breakpoint sessions will automatically have a few + # convenience methods available. See Breakpoint::CommandBundle + # for a list of them. + # + # Breakpoints can also be used remotely over sockets. + # This is implemented by running part of the IRB session + # in the application and part of it in a special client. + # You have to call Breakpoint.activate_drb to enable + # support for remote breakpoints and then run + # breakpoint_client.rb which is distributed with this + # library. See the documentation of Breakpoint.activate_drb + # for details. + def breakpoint(id = nil, context = nil, &block) + callstack = caller + callstack.slice!(0, 3) if callstack.first["breakpoint"] + file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures + + message = "Executing break point " + (id ? "#{id.inspect} " : "") + + "at #{file}:#{line}" + (method ? " in `#{method}'" : "") + + if context then + return handle_breakpoint(context, message, file, line, &block) + end + + Binding.of_caller do |binding_context| + handle_breakpoint(binding_context, message, file, line, &block) + end + end + + module CommandBundle + # Proxy to a Breakpoint client. Lets you directly execute code + # in the context of the client. + class Client + def initialize(eval_handler) # :nodoc: + eval_handler.untaint + @eval_handler = eval_handler + end + + instance_methods.each do |method| + next if method[/^__.+__$/] + undef_method method + end + + # Executes the specified code at the client. + def eval(code) + @eval_handler.call(code) + end + + # Will execute the specified statement at the client. + def method_missing(method, *args, &block) + if args.empty? and not block + result = eval "#{method}" + else + # This is a bit ugly. The alternative would be using an + # eval context instead of an eval handler for executing + # the code at the client. The problem with that approach + # is that we would have to handle special expressions + # like "self", "nil" or constants ourself which is hard. + remote = eval %{ + result = lambda { |block, *args| #{method}(*args, &block) } + def result.call_with_block(*args, &block) + call(block, *args) + end + result + } + remote.call_with_block(*args, &block) + end + + return result + end + end + + # Returns the source code surrounding the location where the + # breakpoint was issued. + def source_lines(context = 5, return_line_numbers = false) + lines = File.readlines(@__bp_file).map { |line| line.chomp } + + break_line = @__bp_line + start_line = [break_line - context, 1].max + end_line = break_line + context + + result = lines[(start_line - 1) .. (end_line - 1)] + + if return_line_numbers then + return [start_line, break_line, result] + else + return result + end + end + + # Lets an object that will forward method calls to the breakpoint + # client. This is useful for outputting longer things at the client + # and so on. You can for example do these things: + # + # client.puts "Hello" # outputs "Hello" at client console + # # outputs "Hello" into the file temp.txt at the client + # client.File.open("temp.txt", "w") { |f| f.puts "Hello" } + def client() + if Breakpoint.use_drb? then + sleep(0.5) until Breakpoint.drb_service.eval_handler + Client.new(Breakpoint.drb_service.eval_handler) + else + Client.new(lambda { |code| eval(code, TOPLEVEL_BINDING) }) + end + end + end + + def handle_breakpoint(context, message, file = "", line = "", &block) # :nodoc: + catch(:debug_return) do |value| + eval(%{ + @__bp_file = #{file.inspect} + @__bp_line = #{line} + extend Breakpoint::CommandBundle + extend DRbUndumped if self + }, context) rescue nil + + if not use_drb? then + puts message + IRB.start(nil, IRB::WorkSpace.new(context)) + else + @drb_service.add_breakpoint(context, message) + end + + block.call if block + end + end + + # These exceptions will be raised on failed asserts + # if Breakpoint.asserts_cause_exceptions is set to + # true. + class FailedAssertError < RuntimeError + end + + # This asserts that the block evaluates to true. + # If it doesn't evaluate to true a breakpoint will + # automatically be created at that execution point. + # + # You can disable assert checking in production + # code by setting Breakpoint.optimize_asserts to + # true. (It will still be enabled when Ruby is run + # via the -d argument.) + # + # Example: + # person_name = "Foobar" + # assert { not person_name.nil? } + # + # Note: If you want to use this method from an + # unit test, you will have to call it by its full + # name, Breakpoint.assert. + def assert(context = nil, &condition) + return if Breakpoint.optimize_asserts and not $DEBUG + return if yield + + callstack = caller + callstack.slice!(0, 3) if callstack.first["assert"] + file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures + + message = "Assert failed at #{file}:#{line}#{" in `#{method}'" if method}." + + if Breakpoint.asserts_cause_exceptions and not $DEBUG then + raise(Breakpoint::FailedAssertError, message) + end + + message += " Executing implicit breakpoint." + + if context then + return handle_breakpoint(context, message, file, line) + end + + Binding.of_caller do |context| + handle_breakpoint(context, message, file, line) + end + end + + # Whether asserts should be ignored if not in debug mode. + # Debug mode can be enabled by running ruby with the -d + # switch or by setting $DEBUG to true. + attr_accessor :optimize_asserts + self.optimize_asserts = false + + # Whether an Exception should be raised on failed asserts + # in non-$DEBUG code or not. By default this is disabled. + attr_accessor :asserts_cause_exceptions + self.asserts_cause_exceptions = false + @use_drb = false + + attr_reader :drb_service # :nodoc: + + class DRbService # :nodoc: + include DRbUndumped + + def initialize + @handler = @eval_handler = @collision_handler = nil + + IRB.instance_eval { @CONF[:RC] = true } + IRB.run_config + end + + def collision + sleep(0.5) until @collision_handler + + @collision_handler.untaint + + @collision_handler.call + end + + def ping() end + + def add_breakpoint(context, message) + workspace = IRB::WorkSpace.new(context) + workspace.extend(DRbUndumped) + + sleep(0.5) until @handler + + @handler.untaint + @handler.call(workspace, message) + end + + attr_accessor :handler, :eval_handler, :collision_handler + end + + # Will run Breakpoint in DRb mode. This will spawn a server + # that can be attached to via the breakpoint-client command + # whenever a breakpoint is executed. This is useful when you + # are debugging CGI applications or other applications where + # you can't access debug sessions via the standard input and + # output of your application. + # + # You can specify an URI where the DRb server will run at. + # This way you can specify the port the server runs on. The + # default URI is druby://localhost:42531. + # + # Please note that breakpoints will be skipped silently in + # case the DRb server can not spawned. (This can happen if + # the port is already used by another instance of your + # application on CGI or another application.) + # + # Also note that by default this will only allow access + # from localhost. You can however specify a list of + # allowed hosts or nil (to allow access from everywhere). + # But that will still not protect you from somebody + # reading the data as it goes through the net. + # + # A good approach for getting security and remote access + # is setting up an SSH tunnel between the DRb service + # and the client. This is usually done like this: + # + # $ ssh -L20000:127.0.0.1:20000 -R10000:127.0.0.1:10000 example.com + # (This will connect port 20000 at the client side to port + # 20000 at the server side, and port 10000 at the server + # side to port 10000 at the client side.) + # + # After that do this on the server side: (the code being debugged) + # Breakpoint.activate_drb("druby://127.0.0.1:20000", "localhost") + # + # And at the client side: + # ruby breakpoint_client.rb -c druby://127.0.0.1:10000 -s druby://127.0.0.1:20000 + # + # Running through such a SSH proxy will also let you use + # breakpoint.rb in case you are behind a firewall. + # + # Detailed information about running DRb through firewalls is + # available at http://www.rubygarden.org/ruby?DrbTutorial + def activate_drb(uri = nil, allowed_hosts = ['localhost', '127.0.0.1', '::1'], + ignore_collisions = false) + + return false if @use_drb + + uri ||= 'druby://localhost:42531' + + if allowed_hosts then + acl = ["deny", "all"] + + Array(allowed_hosts).each do |host| + acl += ["allow", host] + end + + DRb.install_acl(ACL.new(acl)) + end + + @use_drb = true + @drb_service = DRbService.new + did_collision = false + begin + @service = DRb.start_service(uri, @drb_service) + rescue Errno::EADDRINUSE + if ignore_collisions then + nil + else + # The port is already occupied by another + # Breakpoint service. We will try to tell + # the old service that we want its port. + # It will then forward that request to the + # user and retry. + unless did_collision then + DRbObject.new(nil, uri).collision + did_collision = true + end + sleep(10) + retry + end + end + + return true + end + + # Deactivates a running Breakpoint service. + def deactivate_drb + @service.stop_service unless @service.nil? + @service = nil + @use_drb = false + @drb_service = nil + end + + # Returns true when Breakpoints are used over DRb. + # Breakpoint.activate_drb causes this to be true. + def use_drb? + @use_drb == true + end +end + +module IRB # :nodoc: + class << self; remove_method :start; end + def self.start(ap_path = nil, main_context = nil, workspace = nil) + $0 = File::basename(ap_path, ".rb") if ap_path + + # suppress some warnings about redefined constants + old_verbose, $VERBOSE = $VERBOSE, nil + IRB.setup(ap_path) + $VERBOSE = old_verbose + + if @CONF[:SCRIPT] then + irb = Irb.new(main_context, @CONF[:SCRIPT]) + else + irb = Irb.new(main_context) + end + + if workspace then + irb.context.workspace = workspace + end + + @CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC] + @CONF[:MAIN_CONTEXT] = irb.context + + old_sigint = trap("SIGINT") do + begin + irb.signal_handle + rescue RubyLex::TerminateLineInput + # ignored + end + end + + catch(:IRB_EXIT) do + irb.eval_input + end + ensure + trap("SIGINT", old_sigint) + end + + class << self + alias :old_CurrentContext :CurrentContext + remove_method :CurrentContext + end + def IRB.CurrentContext + if old_CurrentContext.nil? and Breakpoint.use_drb? then + result = Object.new + def result.last_value; end + return result + else + old_CurrentContext + end + end + def IRB.parse_opts() end + + class Context #:nodoc: + alias :old_evaluate :evaluate + def evaluate(line, line_no) + if line.chomp == "exit" then + exit + else + old_evaluate(line, line_no) + end + end + end + + class WorkSpace #:nodoc: + alias :old_evaluate :evaluate + + def evaluate(*args) + if Breakpoint.use_drb? then + result = old_evaluate(*args) + if args[0] != :no_proxy and + not [true, false, nil].include?(result) + then + result.extend(DRbUndumped) rescue nil + end + return result + else + old_evaluate(*args) + end + end + end + + module InputCompletor #:nodoc: + def self.eval(code, context, *more) + # Big hack, this assumes that InputCompletor + # will only call eval() when it wants code + # to be executed in the IRB context. + IRB.conf[:MAIN_CONTEXT].workspace.evaluate(:no_proxy, code, *more) + end + end +end + +module DRb #:nodoc: + class DRbObject #:nodoc: + undef :inspect if method_defined?(:inspect) + undef :clone if method_defined?(:clone) + end +end + +# See Breakpoint.breakpoint +def breakpoint(id = nil, &block) + Binding.of_caller do |context| + Breakpoint.breakpoint(id, context, &block) + end +end + +# See Breakpoint.assert +def assert(&block) + Binding.of_caller do |context| + Breakpoint.assert(context, &block) + end +end diff --git a/vendor/rails/railties/lib/breakpoint_client.rb b/vendor/rails/railties/lib/breakpoint_client.rb new file mode 100644 index 00000000..36b7469e --- /dev/null +++ b/vendor/rails/railties/lib/breakpoint_client.rb @@ -0,0 +1,196 @@ +require 'breakpoint' +require 'optparse' +require 'timeout' + +Options = { + :ClientURI => nil, + :ServerURI => "druby://localhost:42531", + :RetryDelay => 2, + :Permanent => true, + :Verbose => false +} + +ARGV.options do |opts| + script_name = File.basename($0) + opts.banner = [ + "Usage: ruby #{script_name} [Options] [server uri]", + "", + "This tool lets you connect to a breakpoint service ", + "which was started via Breakpoint.activate_drb.", + "", + "The server uri defaults to druby://localhost:42531" + ].join("\n") + + opts.separator "" + + opts.on("-c", "--client-uri=uri", + "Run the client on the specified uri.", + "This can be used to specify the port", + "that the client uses to allow for back", + "connections from the server.", + "Default: Find a good URI automatically.", + "Example: -c druby://localhost:12345" + ) { |Options[:ClientURI]| } + + opts.on("-s", "--server-uri=uri", + "Connect to the server specified at the", + "specified uri.", + "Default: druby://localhost:42531" + ) { |Options[:ServerURI]| } + + opts.on("-R", "--retry-delay=delay", Integer, + "Automatically try to reconnect to the", + "server after delay seconds when the", + "connection failed or timed out.", + "A value of 0 disables automatical", + "reconnecting completely.", + "Default: 10" + ) { |Options[:RetryDelay]| } + + opts.on("-P", "--[no-]permanent", + "Run the breakpoint client in permanent mode.", + "This means that the client will keep continue", + "running even after the server has closed the", + "connection. Useful for example in Rails." + ) { |Options[:Permanent]| } + + opts.on("-V", "--[no-]verbose", + "Run the breakpoint client in verbose mode.", + "Will produce more messages, for example between", + "individual breakpoints. This might help in seeing", + "that the breakpoint client is still alive, but adds", + "quite a bit of clutter." + ) { |Options[:Verbose]| } + + opts.separator "" + + opts.on("-h", "--help", + "Show this help message." + ) { puts opts; exit } + opts.on("-v", "--version", + "Display the version information." + ) do + id = %q$Id: breakpoint_client.rb 91 2005-02-04 22:34:08Z flgr $ + puts id.sub("Id: ", "") + puts "(Breakpoint::Version = #{Breakpoint::Version})" + exit + end + + opts.parse! +end + +Options[:ServerURI] = ARGV[0] if ARGV[0] + +module Handlers #:nodoc: + extend self + + def breakpoint_handler(workspace, message) + puts message + IRB.start(nil, nil, workspace) + + puts "" + if Options[:Verbose] then + puts "Resumed execution. Waiting for next breakpoint...", "" + end + end + + def eval_handler(code) + result = eval(code, TOPLEVEL_BINDING) + if result then + DRbObject.new(result) + else + result + end + end + + def collision_handler() + msg = [ + " *** Breakpoint service collision ***", + " Another Breakpoint service tried to use the", + " port already occupied by this one. It will", + " keep waiting until this Breakpoint service", + " is shut down.", + " ", + " If you are using the Breakpoint library for", + " debugging a Rails or other CGI application", + " this likely means that this Breakpoint", + " session belongs to an earlier, outdated", + " request and should be shut down via 'exit'." + ].join("\n") + + if RUBY_PLATFORM["win"] then + # This sucks. Sorry, I'm not doing this because + # I like funky message boxes -- I need to do this + # because on Windows I have no way of displaying + # my notification via puts() when gets() is still + # being performed on STDIN. I have not found a + # better solution. + begin + require 'tk' + root = TkRoot.new { withdraw } + Tk.messageBox('message' => msg, 'type' => 'ok') + root.destroy + rescue Exception + puts "", msg, "" + end + else + puts "", msg, "" + end + end +end + +# Used for checking whether we are currently in the reconnecting loop. +reconnecting = false + +loop do + DRb.start_service(Options[:ClientURI]) + + begin + service = DRbObject.new(nil, Options[:ServerURI]) + + begin + ehandler = Handlers.method(:eval_handler) + chandler = Handlers.method(:collision_handler) + handler = Handlers.method(:breakpoint_handler) + service.eval_handler = ehandler + service.collision_handler = chandler + service.handler = handler + + reconnecting = false + if Options[:Verbose] then + puts "Connection established. Waiting for breakpoint...", "" + end + + loop do + begin + service.ping + rescue DRb::DRbConnError => error + puts "Server exited. Closing connection...", "" + exit! unless Options[:Permanent] + break + end + + sleep(0.5) + end + ensure + service.eval_handler = nil + service.collision_handler = nil + service.handler = nil + end + rescue Exception => error + if Options[:RetryDelay] > 0 then + if not reconnecting then + reconnecting = true + puts "No connection to breakpoint service at #{Options[:ServerURI]} " + + "(#{error.class})" + puts error.backtrace if $DEBUG + puts "Tries to connect will be made every #{Options[:RetryDelay]} seconds..." + end + + sleep Options[:RetryDelay] + retry + else + raise + end + end +end diff --git a/vendor/rails/railties/lib/code_statistics.rb b/vendor/rails/railties/lib/code_statistics.rb new file mode 100644 index 00000000..e99d876e --- /dev/null +++ b/vendor/rails/railties/lib/code_statistics.rb @@ -0,0 +1,107 @@ +class CodeStatistics #:nodoc: + + TEST_TYPES = %w(Units Functionals Unit\ tests Functional\ tests Integration\ tests) + + def initialize(*pairs) + @pairs = pairs + @statistics = calculate_statistics + @total = calculate_total if pairs.length > 1 + end + + def to_s + print_header + @pairs.each { |pair| print_line(pair.first, @statistics[pair.first]) } + print_splitter + + if @total + print_line("Total", @total) + print_splitter + end + + print_code_test_stats + end + + private + def calculate_statistics + @pairs.inject({}) { |stats, pair| stats[pair.first] = calculate_directory_statistics(pair.last); stats } + end + + def calculate_directory_statistics(directory, pattern = /.*\.rb$/) + stats = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0 } + + Dir.foreach(directory) do |file_name| + if File.stat(directory + "/" + file_name).directory? and (/^\./ !~ file_name) + newstats = calculate_directory_statistics(directory + "/" + file_name, pattern) + stats.each { |k, v| stats[k] += newstats[k] } + end + + next unless file_name =~ pattern + + f = File.open(directory + "/" + file_name) + + while line = f.gets + stats["lines"] += 1 + stats["classes"] += 1 if line =~ /class [A-Z]/ + stats["methods"] += 1 if line =~ /def [a-z]/ + stats["codelines"] += 1 unless line =~ /^\s*$/ || line =~ /^\s*#/ + end + end + + stats + end + + def calculate_total + total = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0 } + @statistics.each_value { |pair| pair.each { |k, v| total[k] += v } } + total + end + + def calculate_code + code_loc = 0 + @statistics.each { |k, v| code_loc += v['codelines'] unless TEST_TYPES.include? k } + code_loc + end + + def calculate_tests + test_loc = 0 + @statistics.each { |k, v| test_loc += v['codelines'] if TEST_TYPES.include? k } + test_loc + end + + def print_header + print_splitter + puts "| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |" + print_splitter + end + + def print_splitter + puts "+----------------------+-------+-------+---------+---------+-----+-------+" + end + + def print_line(name, statistics) + m_over_c = (statistics["methods"] / statistics["classes"]) rescue m_over_c = 0 + loc_over_m = (statistics["codelines"] / statistics["methods"]) - 2 rescue loc_over_m = 0 + + start = if TEST_TYPES.include? name + "| #{name.ljust(18)} " + else + "| #{name.ljust(20)} " + end + + puts start + + "| #{statistics["lines"].to_s.rjust(5)} " + + "| #{statistics["codelines"].to_s.rjust(5)} " + + "| #{statistics["classes"].to_s.rjust(7)} " + + "| #{statistics["methods"].to_s.rjust(7)} " + + "| #{m_over_c.to_s.rjust(3)} " + + "| #{loc_over_m.to_s.rjust(5)} |" + end + + def print_code_test_stats + code = calculate_code + tests = calculate_tests + + puts " Code LOC: #{code} Test LOC: #{tests} Code to Test Ratio: 1:#{sprintf("%.1f", tests.to_f/code)}" + puts "" + end + end diff --git a/vendor/rails/railties/lib/commands.rb b/vendor/rails/railties/lib/commands.rb new file mode 100644 index 00000000..841e98a0 --- /dev/null +++ b/vendor/rails/railties/lib/commands.rb @@ -0,0 +1,17 @@ +commands = Dir["#{File.dirname(__FILE__)}/commands/*.rb"].collect { |file_path| File.basename(file_path).split(".").first } + +if commands.include?(ARGV.first) + require "#{File.dirname(__FILE__)}/commands/#{ARGV.shift}" +else + puts <<-USAGE +The 'run' provides a unified access point for all the default Rails' commands. + +Usage: ./script/run [OPTIONS] + +Examples: + ./script/run generate controller Admin + ./script/run process reaper + +USAGE + puts "Choose: #{commands.join(", ")}" +end \ No newline at end of file diff --git a/vendor/rails/railties/lib/commands/about.rb b/vendor/rails/railties/lib/commands/about.rb new file mode 100644 index 00000000..313bc18c --- /dev/null +++ b/vendor/rails/railties/lib/commands/about.rb @@ -0,0 +1,2 @@ +require 'environment' +puts Rails::Info diff --git a/vendor/rails/railties/lib/commands/breakpointer.rb b/vendor/rails/railties/lib/commands/breakpointer.rb new file mode 100644 index 00000000..cc52010c --- /dev/null +++ b/vendor/rails/railties/lib/commands/breakpointer.rb @@ -0,0 +1 @@ +require 'breakpoint_client' diff --git a/vendor/rails/railties/lib/commands/console.rb b/vendor/rails/railties/lib/commands/console.rb new file mode 100644 index 00000000..8b35a8d2 --- /dev/null +++ b/vendor/rails/railties/lib/commands/console.rb @@ -0,0 +1,25 @@ +irb = RUBY_PLATFORM =~ /mswin32/ ? 'irb.bat' : 'irb' + +require 'optparse' +options = { :sandbox => false, :irb => irb } +OptionParser.new do |opt| + opt.banner = "Usage: console [environment] [options]" + opt.on('-s', '--sandbox', 'Rollback database modifications on exit.') { |v| options[:sandbox] = v } + opt.on("--irb=[#{irb}]", 'Invoke a different irb.') { |v| options[:irb] = v } + opt.parse!(ARGV) +end + +libs = " -r irb/completion" +libs << " -r #{RAILS_ROOT}/config/environment" +libs << " -r console_app" +libs << " -r console_sandbox" if options[:sandbox] +libs << " -r console_with_helpers" + +ENV['RAILS_ENV'] = ARGV.first || ENV['RAILS_ENV'] || 'development' +if options[:sandbox] + puts "Loading #{ENV['RAILS_ENV']} environment in sandbox." + puts "Any modifications you make will be rolled back on exit." +else + puts "Loading #{ENV['RAILS_ENV']} environment." +end +exec "#{options[:irb]} #{libs} --simple-prompt" diff --git a/vendor/rails/railties/lib/commands/destroy.rb b/vendor/rails/railties/lib/commands/destroy.rb new file mode 100644 index 00000000..f4b81d65 --- /dev/null +++ b/vendor/rails/railties/lib/commands/destroy.rb @@ -0,0 +1,6 @@ +require "#{RAILS_ROOT}/config/environment" +require 'rails_generator' +require 'rails_generator/scripts/destroy' + +ARGV.shift if ['--help', '-h'].include?(ARGV[0]) +Rails::Generator::Scripts::Destroy.new.run(ARGV) diff --git a/vendor/rails/railties/lib/commands/generate.rb b/vendor/rails/railties/lib/commands/generate.rb new file mode 100755 index 00000000..3d3db3d8 --- /dev/null +++ b/vendor/rails/railties/lib/commands/generate.rb @@ -0,0 +1,6 @@ +require "#{RAILS_ROOT}/config/environment" +require 'rails_generator' +require 'rails_generator/scripts/generate' + +ARGV.shift if ['--help', '-h'].include?(ARGV[0]) +Rails::Generator::Scripts::Generate.new.run(ARGV) diff --git a/vendor/rails/railties/lib/commands/ncgi/listener b/vendor/rails/railties/lib/commands/ncgi/listener new file mode 100644 index 00000000..421c453f --- /dev/null +++ b/vendor/rails/railties/lib/commands/ncgi/listener @@ -0,0 +1,86 @@ +#!/usr/local/bin/ruby + +require 'stringio' +require 'fileutils' +require 'fcgi_handler' + +def message(s) + $stderr.puts "listener: #{s}" if ENV && ENV["DEBUG_GATEWAY"] +end + +class RemoteCGI < CGI + attr_accessor :stdinput, :stdoutput, :env_table + def initialize(env_table, input = nil, output = nil) + self.env_table = env_table + self.stdinput = input || StringIO.new + self.stdoutput = output || StringIO.new + super() + end + + def out(stream) # Ignore the requested output stream + super(stdoutput) + end +end + +class Listener + include DRbUndumped + + def initialize(timeout, socket_path) + @socket = File.expand_path(socket_path) + @mutex = Mutex.new + @active = false + @timeout = timeout + + @handler = RailsFCGIHandler.new + @handler.extend DRbUndumped + + message 'opening socket' + DRb.start_service("drbunix:#{@socket}", self) + + message 'entering process loop' + @handler.process! self + end + + def each_cgi(&cgi_block) + @cgi_block = cgi_block + message 'entering idle loop' + loop do + sleep @timeout rescue nil + die! unless @active + @active = false + end + end + + def process(env, input) + message 'received request' + @mutex.synchronize do + @active = true + + message 'creating input stream' + input_stream = StringIO.new(input) + message 'building CGI instance' + cgi = RemoteCGI.new(eval(env), input_stream) + + message 'yielding to fcgi handler' + @cgi_block.call cgi + message 'yield finished -- sending output' + + cgi.stdoutput.seek(0) + output = cgi.stdoutput.read + + return output + end + end + + def die! + message 'shutting down' + DRb.stop_service + FileUtils.rm_f @socket + Kernel.exit 0 + end +end + +socket_path = ARGV.shift +timeout = (ARGV.shift || 90).to_i + +Listener.new(timeout, socket_path) \ No newline at end of file diff --git a/vendor/rails/railties/lib/commands/ncgi/tracker b/vendor/rails/railties/lib/commands/ncgi/tracker new file mode 100644 index 00000000..859c9fa0 --- /dev/null +++ b/vendor/rails/railties/lib/commands/ncgi/tracker @@ -0,0 +1,69 @@ +#!/usr/local/bin/ruby + +require 'drb' +require 'thread' + +def message(s) + $stderr.puts "tracker: #{s}" if ENV && ENV["DEBUG_GATEWAY"] +end + +class Tracker + include DRbUndumped + + def initialize(instances, socket_path) + @instances = instances + @socket = File.expand_path(socket_path) + @active = false + + @listeners = [] + @instances.times { @listeners << Mutex.new } + + message "using #{@listeners.length} listeners" + message "opening socket at #{@socket}" + + @service = DRb.start_service("drbunix://#{@socket}", self) + end + + def with_listener + message "listener requested" + + mutex = has_lock = index = nil + 3.times do + @listeners.each_with_index do |mutex, index| + has_lock = mutex.try_lock + break if has_lock + end + break if has_lock + sleep 0.05 + end + + if has_lock + message "obtained listener #{index}" + @active = true + begin yield index + ensure + mutex.unlock + message "released listener #{index}" + end + else + message "dropping request because no listeners are available!" + end + end + + def background(check_interval = nil) + if check_interval + loop do + sleep check_interval + message "Idle for #{check_interval}, shutting down" unless @active + @active = false + Kernel.exit 0 + end + else DRb.thread.join + end + end +end + +socket_path = ARGV.shift +instances = ARGV.shift.to_i +t = Tracker.new(instances, socket_path) +t.background(ARGV.first ? ARGV.shift.to_i : 90) \ No newline at end of file diff --git a/vendor/rails/railties/lib/commands/performance/benchmarker.rb b/vendor/rails/railties/lib/commands/performance/benchmarker.rb new file mode 100644 index 00000000..e8804fe1 --- /dev/null +++ b/vendor/rails/railties/lib/commands/performance/benchmarker.rb @@ -0,0 +1,24 @@ +if ARGV.empty? + puts "Usage: ./script/performance/benchmarker [times] 'Person.expensive_way' 'Person.another_expensive_way' ..." + exit 1 +end + +begin + N = Integer(ARGV.first) + ARGV.shift +rescue ArgumentError + N = 1 +end + +require RAILS_ROOT + '/config/environment' +require 'benchmark' +include Benchmark + +# Don't include compilation in the benchmark +ARGV.each { |expression| eval(expression) } + +bm(6) do |x| + ARGV.each_with_index do |expression, idx| + x.report("##{idx + 1}") { N.times { eval(expression) } } + end +end diff --git a/vendor/rails/railties/lib/commands/performance/profiler.rb b/vendor/rails/railties/lib/commands/performance/profiler.rb new file mode 100644 index 00000000..310d6764 --- /dev/null +++ b/vendor/rails/railties/lib/commands/performance/profiler.rb @@ -0,0 +1,34 @@ +if ARGV.empty? + $stderr.puts "Usage: ./script/performance/profiler 'Person.expensive_method(10)' [times]" + exit(1) +end + +# Keep the expensive require out of the profile. +$stderr.puts 'Loading Rails...' +require RAILS_ROOT + '/config/environment' + +# Define a method to profile. +if ARGV[1] and ARGV[1].to_i > 1 + eval "def profile_me() #{ARGV[1]}.times { #{ARGV[0]} } end" +else + eval "def profile_me() #{ARGV[0]} end" +end + +# Use the ruby-prof extension if available. Fall back to stdlib profiler. +begin + require 'prof' + $stderr.puts 'Using the ruby-prof extension.' + Prof.clock_mode = Prof::GETTIMEOFDAY + Prof.start + profile_me + results = Prof.stop + require 'rubyprof_ext' + Prof.print_profile(results, $stderr) +rescue LoadError + require 'profiler' + $stderr.puts 'Using the standard Ruby profiler.' + Profiler__.start_profile + profile_me + Profiler__.stop_profile + Profiler__.print_profile($stderr) +end diff --git a/vendor/rails/railties/lib/commands/plugin.rb b/vendor/rails/railties/lib/commands/plugin.rb new file mode 100644 index 00000000..dd32f541 --- /dev/null +++ b/vendor/rails/railties/lib/commands/plugin.rb @@ -0,0 +1,871 @@ +# Rails Plugin Manager. +# +# Listing available plugins: +# +# $ ./script/plugin list +# continuous_builder http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder +# asset_timestamping http://svn.aviditybytes.com/rails/plugins/asset_timestamping +# enumerations_mixin http://svn.protocool.com/rails/plugins/enumerations_mixin/trunk +# calculations http://techno-weenie.net/svn/projects/calculations/ +# ... +# +# Installing plugins: +# +# $ ./script/plugin install continuous_builder asset_timestamping +# +# Finding Repositories: +# +# $ ./script/plugin discover +# +# Adding Repositories: +# +# $ ./script/plugin source http://svn.protocool.com/rails/plugins/ +# +# How it works: +# +# * Maintains a list of subversion repositories that are assumed to have +# a plugin directory structure. Manage them with the (source, unsource, +# and sources commands) +# +# * The discover command scrapes the following page for things that +# look like subversion repositories with plugins: +# http://wiki.rubyonrails.org/rails/pages/Plugins +# +# * Unless you specify that you want to use svn, script/plugin uses plain old +# HTTP for downloads. The following bullets are true if you specify +# that you want to use svn. +# +# * If `vendor/plugins` is under subversion control, the script will +# modify the svn:externals property and perform an update. You can +# use normal subversion commands to keep the plugins up to date. +# +# * Or, if `vendor/plugins` is not under subversion control, the +# plugin is pulled via `svn checkout` or `svn export` but looks +# exactly the same. +# +# This is Free Software, copyright 2005 by Ryan Tomayko (rtomayko@gmail.com) +# and is licensed MIT: (http://www.opensource.org/licenses/mit-license.php) + +$verbose = false + + +require 'open-uri' +require 'fileutils' +require 'tempfile' + +include FileUtils + +class RailsEnvironment + attr_reader :root + + def initialize(dir) + @root = dir + end + + def self.find(dir=nil) + dir ||= pwd + while dir.length > 1 + return new(dir) if File.exist?(File.join(dir, 'config', 'environment.rb')) + dir = File.dirname(dir) + end + end + + def self.default + @default ||= find + end + + def self.default=(rails_env) + @default = rails_env + end + + def install(name_uri_or_plugin) + if name_uri_or_plugin.is_a? String + if name_uri_or_plugin =~ /:\/\// + plugin = Plugin.new(name_uri_or_plugin) + else + plugin = Plugins[name_uri_or_plugin] + end + else + plugin = name_uri_or_plugin + end + unless plugin.nil? + plugin.install + else + puts "plugin not found: #{name_uri_or_plugin}" + end + end + + def use_svn? + require 'active_support/core_ext/kernel' + silence_stderr {`svn --version` rescue nil} + !$?.nil? && $?.success? + end + + def use_externals? + use_svn? && File.directory?("#{root}/vendor/plugins/.svn") + end + + def use_checkout? + # this is a bit of a guess. we assume that if the rails environment + # is under subversion then they probably want the plugin checked out + # instead of exported. This can be overridden on the command line + File.directory?("#{root}/.svn") + end + + def best_install_method + return :http unless use_svn? + case + when use_externals? then :externals + when use_checkout? then :checkout + else :export + end + end + + def externals + return [] unless use_externals? + ext = `svn propget svn:externals "#{root}/vendor/plugins"` + ext.reject{ |line| line.strip == '' }.map do |line| + line.strip.split(/\s+/, 2) + end + end + + def externals=(items) + unless items.is_a? String + items = items.map{|name,uri| "#{name.ljust(29)} #{uri.chomp('/')}"}.join("\n") + end + Tempfile.open("svn-set-prop") do |file| + file.write(items) + file.flush + system("svn propset -q svn:externals -F #{file.path} \"#{root}/vendor/plugins\"") + end + end + +end + +class Plugin + attr_reader :name, :uri + + def initialize(uri, name=nil) + @uri = uri + guess_name(uri) + end + + def to_s + "#{@name.ljust(30)}#{@uri}" + end + + def installed? + File.directory?("#{rails_env.root}/vendor/plugins/#{name}") \ + or rails_env.externals.detect{ |name, repo| self.uri == repo } + end + + def install(method=nil, options = {}) + method ||= rails_env.best_install_method? + method = :export if method == :http and @uri =~ /svn:\/\/*/ + + uninstall if installed? and options[:force] + + unless installed? + send("install_using_#{method}", options) + run_install_hook + else + puts "already installed: #{name} (#{uri}). pass --force to reinstall" + end + end + + def uninstall + path = "#{rails_env.root}/vendor/plugins/#{name}" + if File.directory?(path) + puts "Removing 'vendor/plugins/#{name}'" if $verbose + rm_r path + else + puts "Plugin doesn't exist: #{path}" + end + # clean up svn:externals + externals = rails_env.externals + externals.reject!{|n,u| name == n or name == u} + rails_env.externals = externals + end + + private + + def run_install_hook + install_hook_file = "#{rails_env.root}/vendor/plugins/#{name}/install.rb" + load install_hook_file if File.exists? install_hook_file + end + + def install_using_export(options = {}) + svn_command :export, options + end + + def install_using_checkout(options = {}) + svn_command :checkout, options + end + + def install_using_externals(options = {}) + externals = rails_env.externals + externals.push([@name, uri]) + rails_env.externals = externals + install_using_checkout(options) + end + + def install_using_http(options = {}) + root = rails_env.root + mkdir_p "#{root}/vendor/plugins" + Dir.chdir "#{root}/vendor/plugins" + puts "fetching from '#{uri}'" if $verbose + fetcher = RecursiveHTTPFetcher.new(uri) + fetcher.quiet = true if options[:quiet] + fetcher.fetch + end + + def svn_command(cmd, options = {}) + root = rails_env.root + mkdir_p "#{root}/vendor/plugins" + base_cmd = "svn #{cmd} #{uri} \"#{root}/vendor/plugins/#{name}\"" + base_cmd += ' -q' if options[:quiet] and not $verbose + base_cmd += " -r #{options[:revision]}" if options[:revision] + puts base_cmd if $verbose + system(base_cmd) + end + + def guess_name(url) + @name = File.basename(url) + if @name == 'trunk' || @name.empty? + @name = File.basename(File.dirname(url)) + end + end + + def rails_env + @rails_env || RailsEnvironment.default + end +end + +class Repositories + include Enumerable + + def initialize(cache_file = File.join(find_home, ".rails-plugin-sources")) + @cache_file = File.expand_path(cache_file) + load! + end + + def each(&block) + @repositories.each(&block) + end + + def add(uri) + unless find{|repo| repo.uri == uri } + @repositories.push(Repository.new(uri)).last + end + end + + def remove(uri) + @repositories.reject!{|repo| repo.uri == uri} + end + + def exist?(uri) + @repositories.detect{|repo| repo.uri == uri } + end + + def all + @repositories + end + + def find_plugin(name) + @repositories.each do |repo| + repo.each do |plugin| + return plugin if plugin.name == name + end + end + return nil + end + + def load! + contents = File.exist?(@cache_file) ? File.read(@cache_file) : defaults + contents = defaults if contents.empty? + @repositories = contents.split(/\n/).reject do |line| + line =~ /^\s*#/ or line =~ /^\s*$/ + end.map { |source| Repository.new(source.strip) } + end + + def save + File.open(@cache_file, 'w') do |f| + each do |repo| + f.write(repo.uri) + f.write("\n") + end + end + end + + def defaults + <<-DEFAULTS + http://dev.rubyonrails.com/svn/rails/plugins/ + DEFAULTS + end + + def find_home + ['HOME', 'USERPROFILE'].each do |homekey| + return ENV[homekey] if ENV[homekey] + end + if ENV['HOMEDRIVE'] && ENV['HOMEPATH'] + return "#{ENV['HOMEDRIVE']}:#{ENV['HOMEPATH']}" + end + begin + File.expand_path("~") + rescue StandardError => ex + if File::ALT_SEPARATOR + "C:/" + else + "/" + end + end + end + + def self.instance + @instance ||= Repositories.new + end + + def self.each(&block) + self.instance.each(&block) + end +end + +class Repository + include Enumerable + attr_reader :uri, :plugins + + def initialize(uri) + @uri = uri.chomp('/') << "/" + @plugins = nil + end + + def plugins + unless @plugins + if $verbose + puts "Discovering plugins in #{@uri}" + puts index + end + + @plugins = index.reject{ |line| line !~ /\/$/ } + @plugins.map! { |name| Plugin.new(File.join(@uri, name), name) } + end + + @plugins + end + + def each(&block) + plugins.each(&block) + end + + private + def index + @index ||= RecursiveHTTPFetcher.new(@uri).ls + end +end + + +# load default environment and parse arguments +require 'optparse' +module Commands + + class Plugin + attr_reader :environment, :script_name, :sources + def initialize + @environment = RailsEnvironment.default + @rails_root = RailsEnvironment.default.root + @script_name = File.basename($0) + @sources = [] + end + + def environment=(value) + @environment = value + RailsEnvironment.default = value + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@script_name} [OPTIONS] command" + o.define_head "Rails plugin manager." + + o.separator "" + o.separator "GENERAL OPTIONS" + + o.on("-r", "--root=DIR", String, + "Set an explicit rails app directory.", + "Default: #{@rails_root}") { |@rails_root| self.environment = RailsEnvironment.new(@rails_root) } + o.on("-s", "--source=URL1,URL2", Array, + "Use the specified plugin repositories instead of the defaults.") { |@sources|} + + o.on("-v", "--verbose", "Turn on verbose output.") { |$verbose| } + o.on("-h", "--help", "Show this help message.") { puts o; exit } + + o.separator "" + o.separator "COMMANDS" + + o.separator " discover Discover plugin repositories." + o.separator " list List available plugins." + o.separator " install Install plugin(s) from known repositories or URLs." + o.separator " update Update installed plugins." + o.separator " remove Uninstall plugins." + o.separator " source Add a plugin source repository." + o.separator " unsource Remove a plugin repository." + o.separator " sources List currently configured plugin repositories." + + o.separator "" + o.separator "EXAMPLES" + o.separator " Install a plugin:" + o.separator " #{@script_name} install continuous_builder\n" + o.separator " Install a plugin from a subversion URL:" + o.separator " #{@script_name} install http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder\n" + o.separator " Install a plugin and add a svn:externals entry to vendor/plugins" + o.separator " #{@script_name} install -x continuous_builder\n" + o.separator " List all available plugins:" + o.separator " #{@script_name} list\n" + o.separator " List plugins in the specified repository:" + o.separator " #{@script_name} list --source=http://dev.rubyonrails.com/svn/rails/plugins/\n" + o.separator " Discover and prompt to add new repositories:" + o.separator " #{@script_name} discover\n" + o.separator " Discover new repositories but just list them, don't add anything:" + o.separator " #{@script_name} discover -l\n" + o.separator " Add a new repository to the source list:" + o.separator " #{@script_name} source http://dev.rubyonrails.com/svn/rails/plugins/\n" + o.separator " Remove a repository from the source list:" + o.separator " #{@script_name} unsource http://dev.rubyonrails.com/svn/rails/plugins/\n" + o.separator " Show currently configured repositories:" + o.separator " #{@script_name} sources\n" + end + end + + def parse!(args=ARGV) + general, sub = split_args(args) + options.parse!(general) + + command = general.shift + if command =~ /^(list|discover|install|source|unsource|sources|remove|update)$/ + command = Commands.const_get(command.capitalize).new(self) + command.parse!(sub) + else + puts "Unknown command: #{command}" + puts options + exit 1 + end + end + + def split_args(args) + left = [] + left << args.shift while args[0] and args[0] =~ /^-/ + left << args.shift if args[0] + return [left, args] + end + + def self.parse!(args=ARGV) + Plugin.new.parse!(args) + end + end + + + class List + def initialize(base_command) + @base_command = base_command + @sources = [] + @local = false + @remote = true + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} list [OPTIONS] [PATTERN]" + o.define_head "List available plugins." + o.separator "" + o.separator "Options:" + o.separator "" + o.on( "-s", "--source=URL1,URL2", Array, + "Use the specified plugin repositories.") {|@sources|} + o.on( "--local", + "List locally installed plugins.") {|@local| @remote = false} + o.on( "--remote", + "List remotely availabled plugins. This is the default behavior", + "unless --local is provided.") {|@remote|} + end + end + + def parse!(args) + options.order!(args) + unless @sources.empty? + @sources.map!{ |uri| Repository.new(uri) } + else + @sources = Repositories.instance.all + end + if @remote + @sources.map{|r| r.plugins}.flatten.each do |plugin| + if @local or !plugin.installed? + puts plugin.to_s + end + end + else + cd "#{@base_command.environment.root}/vendor/plugins" + Dir["*"].select{|p| File.directory?(p)}.each do |name| + puts name + end + end + end + end + + + class Sources + def initialize(base_command) + @base_command = base_command + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} sources [OPTIONS] [PATTERN]" + o.define_head "List configured plugin repositories." + o.separator "" + o.separator "Options:" + o.separator "" + o.on( "-c", "--check", + "Report status of repository.") { |@sources|} + end + end + + def parse!(args) + options.parse!(args) + Repositories.each do |repo| + puts repo.uri + end + end + end + + + class Source + def initialize(base_command) + @base_command = base_command + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} source REPOSITORY" + o.define_head "Add a new repository." + end + end + + def parse!(args) + options.parse!(args) + count = 0 + args.each do |uri| + if Repositories.instance.add(uri) + puts "added: #{uri.ljust(50)}" if $verbose + count += 1 + else + puts "failed: #{uri.ljust(50)}" + end + end + Repositories.instance.save + puts "Added #{count} repositories." + end + end + + + class Unsource + def initialize(base_command) + @base_command = base_command + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} source URI [URI [URI]...]" + o.define_head "Remove repositories from the default search list." + o.separator "" + o.on_tail("-h", "--help", "Show this help message.") { puts o; exit } + end + end + + def parse!(args) + options.parse!(args) + count = 0 + args.each do |uri| + if Repositories.instance.remove(uri) + count += 1 + puts "removed: #{uri.ljust(50)}" + else + puts "failed: #{uri.ljust(50)}" + end + end + Repositories.instance.save + puts "Removed #{count} repositories." + end + end + + + class Discover + def initialize(base_command) + @base_command = base_command + @list = false + @prompt = true + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} discover URI [URI [URI]...]" + o.define_head "Discover repositories referenced on a page." + o.separator "" + o.separator "Options:" + o.separator "" + o.on( "-l", "--list", + "List but don't prompt or add discovered repositories.") { |@list| @prompt = !@list } + o.on( "-n", "--no-prompt", + "Add all new repositories without prompting.") { |v| @prompt = !v } + end + end + + def parse!(args) + options.parse!(args) + args = ['http://wiki.rubyonrails.org/rails/pages/Plugins'] if args.empty? + args.each do |uri| + scrape(uri) do |repo_uri| + catch(:next_uri) do + if @prompt + begin + $stdout.print "Add #{repo_uri}? [Y/n] " + throw :next_uri if $stdin.gets !~ /^y?$/i + rescue Interrupt + $stdout.puts + exit 1 + end + elsif @list + puts repo_uri + throw :next_uri + end + Repositories.instance.add(repo_uri) + puts "discovered: #{repo_uri}" if $verbose or !@prompt + end + end + end + Repositories.instance.save + end + + def scrape(uri) + require 'open-uri' + puts "Scraping #{uri}" if $verbose + dupes = [] + content = open(uri).each do |line| + if line =~ /]*href=['"]([^'"]*)['"]/ or line =~ /(svn:\/\/[^<|\n]*)/ + uri = $1 + if uri =~ /\/plugins\// and uri !~ /\/browser\// + uri = extract_repository_uri(uri) + yield uri unless dupes.include?(uri) or Repositories.instance.exist?(uri) + dupes << uri + end + end + end + end + + def extract_repository_uri(uri) + uri.match(/(svn|https?):.*\/plugins\//i)[0] + end + end + + class Install + def initialize(base_command) + @base_command = base_command + @method = :http + @options = { :quiet => false, :revision => nil, :force => false } + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} install PLUGIN [PLUGIN [PLUGIN] ...]" + o.define_head "Install one or more plugins." + o.separator "" + o.separator "Options:" + o.on( "-x", "--externals", + "Use svn:externals to grab the plugin.", + "Enables plugin updates and plugin versioning.") { |v| @method = :externals } + o.on( "-o", "--checkout", + "Use svn checkout to grab the plugin.", + "Enables updating but does not add a svn:externals entry.") { |v| @method = :checkout } + o.on( "-q", "--quiet", + "Suppresses the output from installation.", + "Ignored if -v is passed (./script/plugin -v install ...)") { |v| @options[:quiet] = true } + o.on( "-r REVISION", "--revision REVISION", + "Checks out the given revision from subversion.", + "Ignored if subversion is not used.") { |v| @options[:revision] = v } + o.on( "-f", "--force", + "Reinstalls a plugin if it's already installed.") { |v| @options[:force] = true } + o.separator "" + o.separator "You can specify plugin names as given in 'plugin list' output or absolute URLs to " + o.separator "a plugin repository." + end + end + + def determine_install_method + best = @base_command.environment.best_install_method + @method = :http if best == :http and @method == :export + case + when (best == :http and @method != :http) + msg = "Cannot install using subversion because `svn' cannot be found in your PATH" + when (best == :export and (@method != :export and @method != :http)) + msg = "Cannot install using #{@method} because this project is not under subversion." + when (best != :externals and @method == :externals) + msg = "Cannot install using externals because vendor/plugins is not under subversion." + end + if msg + puts msg + exit 1 + end + @method + end + + def parse!(args) + options.parse!(args) + environment = @base_command.environment + install_method = determine_install_method + puts "Plugins will be installed using #{install_method}" if $verbose + args.each do |name| + if name =~ /\// then + ::Plugin.new(name).install(install_method, @options) + else + plugin = Repositories.instance.find_plugin(name) + unless plugin.nil? + plugin.install(install_method, @options) + else + puts "Plugin not found: #{name}" + exit 1 + end + end + end + end + end + + class Update + def initialize(base_command) + @base_command = base_command + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} update [name [name]...]" + o.on( "-r REVISION", "--revision REVISION", + "Checks out the given revision from subversion.", + "Ignored if subversion is not used.") { |v| @revision = v } + o.define_head "Update plugins." + end + end + + def parse!(args) + options.parse!(args) + root = @base_command.environment.root + cd root + args = Dir["vendor/plugins/*"].map do |f| + File.directory?("#{f}/.svn") ? File.basename(f) : nil + end.compact if args.empty? + cd "vendor/plugins" + args.each do |name| + if File.directory?(name) + puts "Updating plugin: #{name}" + system("svn #{$verbose ? '' : '-q'} up \"#{name}\" #{@revision ? "-r #{@revision}" : ''}") + else + puts "Plugin doesn't exist: #{name}" + end + end + end + end + + class Remove + def initialize(base_command) + @base_command = base_command + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} remove name [name]..." + o.define_head "Remove plugins." + end + end + + def parse!(args) + options.parse!(args) + root = @base_command.environment.root + args.each do |name| + ::Plugin.new(name).uninstall + end + end + end + +end + +class RecursiveHTTPFetcher + attr_accessor :quiet + def initialize(urls_to_fetch, cwd = ".") + @cwd = cwd + @urls_to_fetch = urls_to_fetch.to_a + @quiet = false + end + + def ls + @urls_to_fetch.collect do |url| + if url =~ /^svn:\/\/.*/ + `svn ls #{url}`.split("\n").map {|entry| "/#{entry}"} rescue nil + else + open(url) do |stream| + links("", stream.read) + end rescue nil + end + end.flatten + end + + def push_d(dir) + @cwd = File.join(@cwd, dir) + FileUtils.mkdir_p(@cwd) + end + + def pop_d + @cwd = File.dirname(@cwd) + end + + def links(base_url, contents) + links = [] + contents.scan(/href\s*=\s*\"*[^\">]*/i) do |link| + link = link.sub(/href="/i, "") + next if link =~ /^http/i || link =~ /^\./ + links << File.join(base_url, link) + end + links + end + + def download(link) + puts "+ #{File.join(@cwd, File.basename(link))}" unless @quiet + open(link) do |stream| + File.open(File.join(@cwd, File.basename(link)), "wb") do |file| + file.write(stream.read) + end + end + end + + def fetch(links = @urls_to_fetch) + links.each do |l| + (l =~ /\/$/ || links == @urls_to_fetch) ? fetch_dir(l) : download(l) + end + end + + def fetch_dir(url) + push_d(File.basename(url)) + open(url) do |stream| + contents = stream.read + fetch(links(url, contents)) + end + pop_d + end +end + +Commands::Plugin.parse! diff --git a/vendor/rails/railties/lib/commands/process/reaper.rb b/vendor/rails/railties/lib/commands/process/reaper.rb new file mode 100644 index 00000000..d0126c72 --- /dev/null +++ b/vendor/rails/railties/lib/commands/process/reaper.rb @@ -0,0 +1,130 @@ +require 'optparse' +require 'net/http' +require 'uri' + +if RUBY_PLATFORM =~ /mswin32/ then abort("Reaper is only for Unix") end + +# Instances of this class represent a single running process. Processes may +# be queried by "keyword" to find those that meet a specific set of criteria. +class ProgramProcess + class << self + + # Searches for all processes matching the given keywords, and then invokes + # a specific action on each of them. This is useful for (e.g.) reloading a + # set of processes: + # + # ProgramProcess.process_keywords(:reload, "basecamp") + def process_keywords(action, *keywords) + processes = keywords.collect { |keyword| find_by_keyword(keyword) }.flatten + + if processes.empty? + puts "Couldn't find any process matching: #{keywords.join(" or ")}" + else + processes.each do |process| + puts "#{action.capitalize}ing #{process}" + process.send(action) + end + end + end + + # Searches for all processes matching the given keyword: + # + # ProgramProcess.find_by_keyword("basecamp") + def find_by_keyword(keyword) + process_lines_with_keyword(keyword).split("\n").collect { |line| + next if line =~ /inq|ps axww|grep|spawn-fcgi|spawner|reaper/ + pid, *command = line.split + new(pid, command.join(" ")) + }.compact + end + + private + def process_lines_with_keyword(keyword) + `ps axww -o 'pid command' | grep #{keyword}` + end + end + + # Create a new ProgramProcess instance that represents the process with the + # given pid, running the given command. + def initialize(pid, command) + @pid, @command = pid, command + end + + # Forces the (rails) application to reload by sending a +HUP+ signal to the + # process. + def reload + `kill -s HUP #{@pid}` + end + + # Forces the (rails) application to gracefully terminate by sending a + # +TERM+ signal to the process. + def graceful + `kill -s TERM #{@pid}` + end + + # Forces the (rails) application to terminate immediately by sending a -9 + # signal to the process. + def kill + `kill -9 #{@pid}` + end + + # Send a +USR1+ signal to the process. + def usr1 + `kill -s USR1 #{@pid}` + end + + # Force the (rails) application to restart by sending a +USR2+ signal to the + # process. + def restart + `kill -s USR2 #{@pid}` + end + + def to_s #:nodoc: + "[#{@pid}] #{@command}" + end +end + +OPTIONS = { + :action => "restart", + :dispatcher => File.expand_path(RAILS_ROOT + '/public/dispatch.fcgi') +} + +ARGV.options do |opts| + opts.banner = "Usage: reaper [options]" + + opts.separator "" + + opts.on <<-EOF + Description: + The reaper is used to restart, reload, gracefully exit, and forcefully exit FCGI processes + running a Rails Dispatcher. This is commonly done when a new version of the application + is available, so the existing processes can be updated to use the latest code. + + The reaper actions are: + + * restart : Restarts the application by reloading both application and framework code + * reload : Only reloads the application, but not the framework (like the development environment) + * graceful: Marks all of the processes for exit after the next request + * kill : Forcefully exists all processes regardless of whether they're currently serving a request + + Restart is the most common and default action. + + Examples: + reaper # restarts the default dispatcher + reaper -a reload + reaper -a exit -d /my/special/dispatcher.fcgi + EOF + + opts.on(" Options:") + + opts.on("-a", "--action=name", "reload|graceful|kill (default: #{OPTIONS[:action]})", String) { |v| OPTIONS[:action] = v } + opts.on("-d", "--dispatcher=path", "default: #{OPTIONS[:dispatcher]}", String) { |v| OPTIONS[:dispatcher] = v } + + opts.separator "" + + opts.on("-h", "--help", "Show this help message.") { puts opts; exit } + + opts.parse! +end + +ProgramProcess.process_keywords(OPTIONS[:action], OPTIONS[:dispatcher]) \ No newline at end of file diff --git a/vendor/rails/railties/lib/commands/process/spawner.rb b/vendor/rails/railties/lib/commands/process/spawner.rb new file mode 100644 index 00000000..8c76a1c7 --- /dev/null +++ b/vendor/rails/railties/lib/commands/process/spawner.rb @@ -0,0 +1,94 @@ +require 'optparse' +require 'socket' + +def daemonize #:nodoc: + exit if fork # Parent exits, child continues. + Process.setsid # Become session leader. + exit if fork # Zap session leader. See [1]. + Dir.chdir "/" # Release old working directory. + File.umask 0000 # Ensure sensible umask. Adjust as needed. + STDIN.reopen "/dev/null" # Free file descriptors and + STDOUT.reopen "/dev/null", "a" # point them somewhere sensible. + STDERR.reopen STDOUT # STDOUT/ERR should better go to a logfile. +end + +def spawn(port) + print "Checking if something is already running on port #{port}..." + begin + srv = TCPServer.new('0.0.0.0', port) + srv.close + srv = nil + print "NO\n " + print "Starting FCGI on port: #{port}\n " + system("#{OPTIONS[:spawner]} -f #{OPTIONS[:dispatcher]} -p #{port}") + rescue + print "YES\n" + end +end + +def spawn_all + OPTIONS[:instances].times { |i| spawn(OPTIONS[:port] + i) } +end + +OPTIONS = { + :environment => "production", + :spawner => '/usr/bin/env spawn-fcgi', + :dispatcher => File.expand_path(RAILS_ROOT + '/public/dispatch.fcgi'), + :port => 8000, + :instances => 3, + :repeat => nil +} + +ARGV.options do |opts| + opts.banner = "Usage: spawner [options]" + + opts.separator "" + + opts.on <<-EOF + Description: + The spawner is a wrapper for spawn-fcgi that makes it easier to start multiple FCGI + processes running the Rails dispatcher. The spawn-fcgi command is included with the lighttpd + web server, but can be used with both Apache and lighttpd (and any other web server supporting + externally managed FCGI processes). + + You decide a starting port (default is 8000) and the number of FCGI process instances you'd + like to run. So if you pick 9100 and 3 instances, you'll start processes on 9100, 9101, and 9102. + + By setting the repeat option, you get a protection loop, which will attempt to restart any FCGI processes + that might have been exited or outright crashed. + + Examples: + spawner # starts instances on 8000, 8001, and 8002 + spawner -p 9100 -i 10 # starts 10 instances counting from 9100 to 9109 + spawner -p 9100 -r 5 # starts 3 instances counting from 9100 to 9102 and attempts start them every 5 seconds + EOF + + opts.on(" Options:") + + opts.on("-p", "--port=number", Integer, "Starting port number (default: #{OPTIONS[:port]})") { |v| OPTIONS[:port] = v } + opts.on("-i", "--instances=number", Integer, "Number of instances (default: #{OPTIONS[:instances]})") { |v| OPTIONS[:instances] = v } + opts.on("-r", "--repeat=seconds", Integer, "Repeat spawn attempts every n seconds (default: off)") { |v| OPTIONS[:repeat] = v } + opts.on("-e", "--environment=name", String, "test|development|production (default: #{OPTIONS[:environment]})") { |v| OPTIONS[:environment] = v } + opts.on("-s", "--spawner=path", String, "default: #{OPTIONS[:spawner]}") { |v| OPTIONS[:spawner] = v } + opts.on("-d", "--dispatcher=path", String, "default: #{OPTIONS[:dispatcher]}") { |dispatcher| OPTIONS[:dispatcher] = File.expand_path(dispatcher) } + + opts.separator "" + + opts.on("-h", "--help", "Show this help message.") { puts opts; exit } + + opts.parse! +end + +ENV["RAILS_ENV"] = OPTIONS[:environment] + +if OPTIONS[:repeat] + daemonize + trap("TERM") { exit } + + loop do + spawn_all + sleep(OPTIONS[:repeat]) + end +else + spawn_all +end diff --git a/vendor/rails/railties/lib/commands/process/spinner.rb b/vendor/rails/railties/lib/commands/process/spinner.rb new file mode 100644 index 00000000..c0b2f09a --- /dev/null +++ b/vendor/rails/railties/lib/commands/process/spinner.rb @@ -0,0 +1,57 @@ +require 'optparse' + +def daemonize #:nodoc: + exit if fork # Parent exits, child continues. + Process.setsid # Become session leader. + exit if fork # Zap session leader. See [1]. + Dir.chdir "/" # Release old working directory. + File.umask 0000 # Ensure sensible umask. Adjust as needed. + STDIN.reopen "/dev/null" # Free file descriptors and + STDOUT.reopen "/dev/null", "a" # point them somewhere sensible. + STDERR.reopen STDOUT # STDOUT/ERR should better go to a logfile. +end + +OPTIONS = { + :interval => 5.0, + :command => File.expand_path(RAILS_ROOT + '/script/process/spawner'), + :daemon => false +} + +ARGV.options do |opts| + opts.banner = "Usage: spinner [options]" + + opts.separator "" + + opts.on <<-EOF + Description: + The spinner is a protection loop for the spawner, which will attempt to restart any FCGI processes + that might have been exited or outright crashed. It's a brute-force attempt that'll just try + to run the spawner every X number of seconds, so it does pose a light load on the server. + + Examples: + spinner # attempts to run the spawner with default settings every second with output on the terminal + spinner -i 3 -d # only run the spawner every 3 seconds and detach from the terminal to become a daemon + spinner -c '/path/to/app/script/process/spawner -p 9000 -i 10' -d # using custom spawner + EOF + + opts.on(" Options:") + + opts.on("-c", "--command=path", String) { |v| OPTIONS[:command] = v } + opts.on("-i", "--interval=seconds", Float) { |v| OPTIONS[:interval] = v } + opts.on("-d", "--daemon") { |v| OPTIONS[:daemon] = v } + + opts.separator "" + + opts.on("-h", "--help", "Show this help message.") { puts opts; exit } + + opts.parse! +end + +daemonize if OPTIONS[:daemon] + +trap(OPTIONS[:daemon] ? "TERM" : "INT") { exit } + +loop do + system(OPTIONS[:command]) + sleep(OPTIONS[:interval]) +end \ No newline at end of file diff --git a/vendor/rails/railties/lib/commands/runner.rb b/vendor/rails/railties/lib/commands/runner.rb new file mode 100644 index 00000000..47186d52 --- /dev/null +++ b/vendor/rails/railties/lib/commands/runner.rb @@ -0,0 +1,27 @@ +require 'optparse' + +options = { :environment => (ENV['RAILS_ENV'] || "development").dup } + +ARGV.options do |opts| + script_name = File.basename($0) + opts.banner = "Usage: runner 'puts Person.find(1).name' [options]" + + opts.separator "" + + opts.on("-e", "--environment=name", String, + "Specifies the environment for the runner to operate under (test/development/production).", + "Default: development") { |v| options[:environment] = v } + + opts.separator "" + + opts.on("-h", "--help", + "Show this help message.") { puts opts; exit } + + opts.parse! +end + +ENV["RAILS_ENV"] = options[:environment] +RAILS_ENV.replace(options[:environment]) if defined?(RAILS_ENV) + +require RAILS_ROOT + '/config/environment' +ARGV.empty? ? puts("Usage: runner 'code' [options]") : eval(ARGV.first) diff --git a/vendor/rails/railties/lib/commands/server.rb b/vendor/rails/railties/lib/commands/server.rb new file mode 100644 index 00000000..82a3e108 --- /dev/null +++ b/vendor/rails/railties/lib/commands/server.rb @@ -0,0 +1,30 @@ +require 'active_support' +require 'fileutils' + +begin + require_library_or_gem 'fcgi' +rescue Exception + # FCGI not available +end + +server = case ARGV.first + when "lighttpd" + ARGV.shift + when "webrick" + ARGV.shift + else + if RUBY_PLATFORM !~ /mswin/ && !silence_stderr { `lighttpd -version` }.blank? && defined?(FCGI) + "lighttpd" + else + "webrick" + end +end + +if server == "webrick" + puts "=> Booting WEBrick..." +else + puts "=> Booting lighttpd (use 'script/server webrick' to force WEBrick)" +end + +FileUtils.mkdir_p(%w( tmp/sessions tmp/cache tmp/sockets )) +require "commands/servers/#{server}" diff --git a/vendor/rails/railties/lib/commands/servers/lighttpd.rb b/vendor/rails/railties/lib/commands/servers/lighttpd.rb new file mode 100644 index 00000000..34b51f21 --- /dev/null +++ b/vendor/rails/railties/lib/commands/servers/lighttpd.rb @@ -0,0 +1,92 @@ +require 'rbconfig' + +unless RUBY_PLATFORM !~ /mswin/ && !silence_stderr { `lighttpd -version` }.blank? + puts "PROBLEM: Lighttpd is not available on your system (or not in your path)" + exit 1 +end + +unless defined?(FCGI) + puts "PROBLEM: Lighttpd requires that the FCGI Ruby bindings are installed on the system" + exit 1 +end + +require 'initializer' +configuration = Rails::Initializer.run(:initialize_logger).configuration +default_config_file = config_file = Pathname.new("#{RAILS_ROOT}/config/lighttpd.conf").cleanpath + +require 'optparse' + +detach = false + +ARGV.options do |opt| + opt.on('-c', "--config=#{config_file}", 'Specify a different lighttpd config file.') { |path| config_file = path } + opt.on('-h', '--help', 'Show this message.') { puts opt; exit 0 } + opt.on('-d', '-d', 'Call with -d to detach') { detach = true; puts "=> Configuration in config/lighttpd.conf" } + opt.parse! +end + +unless File.exist?(config_file) + if config_file != default_config_file + puts "=> #{config_file} not found." + exit 1 + end + + require 'fileutils' + + source = File.expand_path(File.join(File.dirname(__FILE__), + "..", "..", "..", "configs", "lighttpd.conf")) + puts "=> #{config_file} not found, copying from #{source}" + + FileUtils.cp(source, config_file) +end + +config = IO.read(config_file) +default_port, default_ip = 3000, '0.0.0.0' +port = config.scan(/^\s*server.port\s*=\s*(\d+)/).first rescue default_port +ip = config.scan(/^\s*server.bind\s*=\s*"([^"]+)"/).first rescue default_ip +puts "=> Rails application started on http://#{ip || default_ip}:#{port || default_port}" + +tail_thread = nil + +if !detach + puts "=> Call with -d to detach" + puts "=> Ctrl-C to shutdown server (see config/lighttpd.conf for options)" + detach = false + + cursor = File.size(configuration.log_path) + last_checked = Time.now + tail_thread = Thread.new do + File.open(configuration.log_path, 'r') do |f| + loop do + f.seek cursor + if f.mtime > last_checked + last_checked = f.mtime + contents = f.read + cursor += contents.length + print contents + end + sleep 1 + end + end + end +end + +trap(:INT) { exit } + +begin + `rake tmp:sockets:clear` # Needed if lighttpd crashes or otherwise leaves FCGI sockets around + `lighttpd #{!detach ? "-D " : ""}-f #{config_file}` +ensure + unless detach + tail_thread.kill if tail_thread + puts 'Exiting' + + # Ensure FCGI processes are reaped + silence_stream(STDOUT) do + ARGV.replace ['-a', 'kill'] + require 'commands/process/reaper' + end + + `rake tmp:sockets:clear` # Remove sockets on clean shutdown + end +end diff --git a/vendor/rails/railties/lib/commands/servers/webrick.rb b/vendor/rails/railties/lib/commands/servers/webrick.rb new file mode 100644 index 00000000..3fddcc54 --- /dev/null +++ b/vendor/rails/railties/lib/commands/servers/webrick.rb @@ -0,0 +1,59 @@ +require 'webrick' +require 'optparse' + +OPTIONS = { + :port => 3000, + :ip => "0.0.0.0", + :environment => (ENV['RAILS_ENV'] || "development").dup, + :server_root => File.expand_path(RAILS_ROOT + "/public/"), + :server_type => WEBrick::SimpleServer, + :charset => "UTF-8", + :mime_types => WEBrick::HTTPUtils::DefaultMimeTypes +} + +ARGV.options do |opts| + script_name = File.basename($0) + opts.banner = "Usage: ruby #{script_name} [options]" + + opts.separator "" + + opts.on("-p", "--port=port", Integer, + "Runs Rails on the specified port.", + "Default: 3000") { |v| OPTIONS[:port] = v } + opts.on("-b", "--binding=ip", String, + "Binds Rails to the specified ip.", + "Default: 0.0.0.0") { |v| OPTIONS[:ip] = v } + opts.on("-e", "--environment=name", String, + "Specifies the environment to run this server under (test/development/production).", + "Default: development") { |v| OPTIONS[:environment] = v } + opts.on("-m", "--mime-types=filename", String, + "Specifies an Apache style mime.types configuration file to be used for mime types", + "Default: none") { |mime_types_file| OPTIONS[:mime_types] = WEBrick::HTTPUtils::load_mime_types(mime_types_file) } + + opts.on("-d", "--daemon", + "Make Rails run as a Daemon (only works if fork is available -- meaning on *nix)." + ) { OPTIONS[:server_type] = WEBrick::Daemon } + + opts.on("-c", "--charset=charset", String, + "Set default charset for output.", + "Default: UTF-8") { |v| OPTIONS[:charset] = v } + + opts.separator "" + + opts.on("-h", "--help", + "Show this help message.") { puts opts; exit } + + opts.parse! +end + +ENV["RAILS_ENV"] = OPTIONS[:environment] +RAILS_ENV.replace(OPTIONS[:environment]) if defined?(RAILS_ENV) + +require RAILS_ROOT + "/config/environment" +require 'webrick_server' + +OPTIONS['working_directory'] = File.expand_path(RAILS_ROOT) + +puts "=> Rails application started on http://#{OPTIONS[:ip]}:#{OPTIONS[:port]}" +puts "=> Ctrl-C to shutdown server; call with --help for options" if OPTIONS[:server_type] == WEBrick::SimpleServer +DispatchServlet.dispatch(OPTIONS) diff --git a/vendor/rails/railties/lib/commands/update.rb b/vendor/rails/railties/lib/commands/update.rb new file mode 100644 index 00000000..83ef8333 --- /dev/null +++ b/vendor/rails/railties/lib/commands/update.rb @@ -0,0 +1,4 @@ +require "#{RAILS_ROOT}/config/environment" +require 'rails_generator' +require 'rails_generator/scripts/update' +Rails::Generator::Scripts::Update.new.run(ARGV) diff --git a/vendor/rails/railties/lib/console_app.rb b/vendor/rails/railties/lib/console_app.rb new file mode 100644 index 00000000..4c212605 --- /dev/null +++ b/vendor/rails/railties/lib/console_app.rb @@ -0,0 +1,27 @@ +require 'action_controller/integration' + +# work around the at_exit hook in test/unit, which kills IRB +Test::Unit.run = true + +# reference the global "app" instance, created on demand. To recreate the +# instance, pass a non-false value as the parameter. +def app(create=false) + @app_integration_instance = nil if create + @app_integration_instance ||= new_session do |sess| + sess.host! "www.example.com" + end +end + +# create a new session. If a block is given, the new session will be yielded +# to the block before being returned. +def new_session + session = ActionController::Integration::Session.new + yield session if block_given? + session +end + +#reloads the environment +def reload! + puts "Reloading..." + Dispatcher.reset_application! +end diff --git a/vendor/rails/railties/lib/console_sandbox.rb b/vendor/rails/railties/lib/console_sandbox.rb new file mode 100644 index 00000000..80f3dbc2 --- /dev/null +++ b/vendor/rails/railties/lib/console_sandbox.rb @@ -0,0 +1,6 @@ +ActiveRecord::Base.lock_mutex +ActiveRecord::Base.connection.begin_db_transaction +at_exit do + ActiveRecord::Base.connection.rollback_db_transaction + ActiveRecord::Base.unlock_mutex +end diff --git a/vendor/rails/railties/lib/console_with_helpers.rb b/vendor/rails/railties/lib/console_with_helpers.rb new file mode 100644 index 00000000..66a4248e --- /dev/null +++ b/vendor/rails/railties/lib/console_with_helpers.rb @@ -0,0 +1,23 @@ +class Module + def include_all_modules_from(parent_module) + parent_module.constants.each do |const| + mod = parent_module.const_get(const) + if mod.class == Module + send(:include, mod) + include_all_modules_from(mod) + end + end + end +end + +def helper + @helper_proxy ||= Object.new +end + +require 'application' + +class << helper + include_all_modules_from ActionView +end + +@controller = ApplicationController.new diff --git a/vendor/rails/railties/lib/dispatcher.rb b/vendor/rails/railties/lib/dispatcher.rb new file mode 100644 index 00000000..721a367f --- /dev/null +++ b/vendor/rails/railties/lib/dispatcher.rb @@ -0,0 +1,117 @@ +#-- +# Copyright (c) 2004 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +# This class provides an interface for dispatching a CGI (or CGI-like) request +# to the appropriate controller and action. It also takes care of resetting +# the environment (when Dependencies.load? is true) after each request. +class Dispatcher + class << self + + # Dispatch the given CGI request, using the given session options, and + # emitting the output via the given output. If you dispatch with your + # own CGI object be sure to handle the exceptions it raises on multipart + # requests (EOFError and ArgumentError). + def dispatch(cgi = nil, session_options = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout) + if cgi ||= new_cgi(output) + request, response = ActionController::CgiRequest.new(cgi, session_options), ActionController::CgiResponse.new(cgi) + prepare_application + ActionController::Routing::Routes.recognize!(request).process(request, response).out(output) + end + rescue Object => exception + failsafe_response(output, '500 Internal Server Error', exception) do + ActionController::Base.process_with_exception(request, response, exception).out(output) + end + ensure + # Do not give a failsafe response here. + reset_after_dispatch + end + + # Reset the application by clearing out loaded controllers, views, actions, + # mailers, and so forth. This allows them to be loaded again without having + # to restart the server (WEBrick, FastCGI, etc.). + def reset_application! + Dependencies.clear + ActiveRecord::Base.reset_subclasses + Class.remove_class(*Reloadable.reloadable_classes) + end + + private + # CGI.new plus exception handling. CGI#read_multipart raises EOFError + # if body.empty? or body.size != Content-Length and raises ArgumentError + # if Content-Length is non-integer. + def new_cgi(output) + failsafe_response(output, '400 Bad Request') { CGI.new } + end + + def prepare_application + ActionController::Routing::Routes.reload if Dependencies.load? + prepare_breakpoint + require_dependency('application.rb') unless Object.const_defined?(:ApplicationController) + ActiveRecord::Base.verify_active_connections! + end + + def reset_after_dispatch + reset_application! if Dependencies.load? + Breakpoint.deactivate_drb if defined?(BREAKPOINT_SERVER_PORT) + end + + def prepare_breakpoint + return unless defined?(BREAKPOINT_SERVER_PORT) + require 'breakpoint' + Breakpoint.activate_drb("druby://localhost:#{BREAKPOINT_SERVER_PORT}", nil, !defined?(FastCGI)) + true + rescue + nil + end + + # If the block raises, send status code as a last-ditch response. + def failsafe_response(output, status, exception = nil) + yield + rescue Object + begin + output.write "Status: #{status}\r\n" + + if exception + message = exception.to_s + "\r\n" + exception.backtrace.join("\r\n") + error_path = File.join(RAILS_ROOT, 'public', '500.html') + + if defined?(RAILS_DEFAULT_LOGGER) && !RAILS_DEFAULT_LOGGER.nil? + RAILS_DEFAULT_LOGGER.fatal(message) + + output.write "Content-Type: text/html\r\n\r\n" + + if File.exists?(error_path) + output.write(IO.read(error_path)) + else + output.write("

      Application error (Rails)

      ") + end + else + output.write "Content-Type: text/plain\r\n\r\n" + output.write(message) + end + end + rescue Object + end + end + end +end diff --git a/vendor/rails/railties/lib/fcgi_handler.rb b/vendor/rails/railties/lib/fcgi_handler.rb new file mode 100644 index 00000000..10f846fa --- /dev/null +++ b/vendor/rails/railties/lib/fcgi_handler.rb @@ -0,0 +1,207 @@ +require 'fcgi' +require 'logger' +require 'dispatcher' +require 'rbconfig' + +class RailsFCGIHandler + SIGNALS = { + 'HUP' => :reload, + 'TERM' => :exit_now, + 'USR1' => :exit, + 'USR2' => :restart, + 'SIGTRAP' => :breakpoint + } + + attr_reader :when_ready + + attr_accessor :log_file_path + attr_accessor :gc_request_period + + + # Initialize and run the FastCGI instance, passing arguments through to new. + def self.process!(*args, &block) + new(*args, &block).process! + end + + # Initialize the FastCGI instance with the path to a crash log + # detailing unhandled exceptions (default RAILS_ROOT/log/fastcgi.crash.log) + # and the number of requests to process between garbage collection runs + # (default nil for normal GC behavior.) Optionally, pass a block which + # takes this instance as an argument for further configuration. + def initialize(log_file_path = nil, gc_request_period = nil) + self.log_file_path = log_file_path || "#{RAILS_ROOT}/log/fastcgi.crash.log" + self.gc_request_period = gc_request_period + + # Yield for additional configuration. + yield self if block_given? + + # Safely install signal handlers. + install_signal_handlers + + # Start error timestamp at 11 seconds ago. + @last_error_on = Time.now - 11 + + dispatcher_log :info, "starting" + end + + def process!(provider = FCGI) + # Make a note of $" so we can safely reload this instance. + mark! + + run_gc! if gc_request_period + + provider.each_cgi do |cgi| + process_request(cgi) + + case when_ready + when :reload + reload! + when :restart + close_connection(cgi) + restart! + when :exit + close_connection(cgi) + break + when :breakpoint + close_connection(cgi) + breakpoint! + end + + gc_countdown + end + + GC.enable + dispatcher_log :info, "terminated gracefully" + + rescue SystemExit => exit_error + dispatcher_log :info, "terminated by explicit exit" + + rescue Object => fcgi_error + # retry on errors that would otherwise have terminated the FCGI process, + # but only if they occur more than 10 seconds apart. + if !(SignalException === fcgi_error) && Time.now - @last_error_on > 10 + @last_error_on = Time.now + dispatcher_error(fcgi_error, "almost killed by this error") + retry + else + dispatcher_error(fcgi_error, "killed by this error") + end + end + + + private + def logger + @logger ||= Logger.new(@log_file_path) + end + + def dispatcher_log(level, msg) + time_str = Time.now.strftime("%d/%b/%Y:%H:%M:%S") + logger.send(level, "[#{time_str} :: #{$$}] #{msg}") + rescue Object => log_error + STDERR << "Couldn't write to #{@log_file_path.inspect}: #{msg}\n" + STDERR << " #{log_error.class}: #{log_error.message}\n" + end + + def dispatcher_error(e, msg = "") + error_message = + "Dispatcher failed to catch: #{e} (#{e.class})\n" + + " #{e.backtrace.join("\n ")}\n#{msg}" + dispatcher_log(:error, error_message) + end + + def install_signal_handlers + SIGNALS.each do |signal, handler_name| + install_signal_handler(signal, method("#{handler_name}_handler").to_proc) + end + end + + def install_signal_handler(signal, handler) + trap(signal, handler) + rescue ArgumentError + dispatcher_log :warn, "Ignoring unsupported signal #{signal}." + end + + def exit_now_handler(signal) + dispatcher_log :info, "asked to terminate immediately" + exit + end + + def exit_handler(signal) + dispatcher_log :info, "asked to terminate ASAP" + @when_ready = :exit + end + + def reload_handler(signal) + dispatcher_log :info, "asked to reload ASAP" + @when_ready = :reload + end + + def restart_handler(signal) + dispatcher_log :info, "asked to restart ASAP" + @when_ready = :restart + end + + def breakpoint_handler(signal) + dispatcher_log :info, "asked to breakpoint ASAP" + @when_ready = :breakpoint + end + + def process_request(cgi) + Dispatcher.dispatch(cgi) + rescue Object => e + raise if SignalException === e + dispatcher_error(e) + end + + def restart! + config = ::Config::CONFIG + ruby = File::join(config['bindir'], config['ruby_install_name']) + config['EXEEXT'] + command_line = [ruby, $0, ARGV].flatten.join(' ') + + dispatcher_log :info, "restarted" + + exec(command_line) + end + + def reload! + run_gc! if gc_request_period + restore! + @when_ready = nil + dispatcher_log :info, "reloaded" + end + + def mark! + @features = $".clone + end + + def restore! + $".replace @features + Dispatcher.reset_application! + ActionController::Routing::Routes.reload + end + + def breakpoint! + require 'breakpoint' + port = defined?(BREAKPOINT_SERVER_PORT) ? BREAKPOINT_SERVER_PORT : 42531 + Breakpoint.activate_drb("druby://localhost:#{port}", nil, !defined?(FastCGI)) + dispatcher_log :info, "breakpointing" + breakpoint + @when_ready = nil + end + + def run_gc! + @gc_request_countdown = gc_request_period + GC.enable; GC.start; GC.disable + end + + def gc_countdown + if gc_request_period + @gc_request_countdown -= 1 + run_gc! if @gc_request_countdown <= 0 + end + end + + def close_connection(cgi) + cgi.instance_variable_get("@request").finish + end +end \ No newline at end of file diff --git a/vendor/rails/railties/lib/initializer.rb b/vendor/rails/railties/lib/initializer.rb new file mode 100644 index 00000000..eed5c16d --- /dev/null +++ b/vendor/rails/railties/lib/initializer.rb @@ -0,0 +1,622 @@ +require 'logger' +require 'set' +require File.join(File.dirname(__FILE__), 'railties_path') + +RAILS_ENV = (ENV['RAILS_ENV'] || 'development').dup unless defined?(RAILS_ENV) + +module Rails + # The Initializer is responsible for processing the Rails configuration, such + # as setting the $LOAD_PATH, requiring the right frameworks, initializing + # logging, and more. It can be run either as a single command that'll just + # use the default configuration, like this: + # + # Rails::Initializer.run + # + # But normally it's more interesting to pass in a custom configuration + # through the block running: + # + # Rails::Initializer.run do |config| + # config.frameworks -= [ :action_web_service ] + # end + # + # This will use the default configuration options from Rails::Configuration, + # but allow for overwriting on select areas. + class Initializer + # The Configuration instance used by this Initializer instance. + attr_reader :configuration + + # The set of loaded plugins. + attr_reader :loaded_plugins + + # Runs the initializer. By default, this will invoke the #process method, + # which simply executes all of the initialization routines. Alternately, + # you can specify explicitly which initialization routine you want: + # + # Rails::Initializer.run(:set_load_path) + # + # This is useful if you only want the load path initialized, without + # incuring the overhead of completely loading the entire environment. + def self.run(command = :process, configuration = Configuration.new) + yield configuration if block_given? + initializer = new configuration + initializer.send(command) + initializer + end + + # Create a new Initializer instance that references the given Configuration + # instance. + def initialize(configuration) + @configuration = configuration + @loaded_plugins = Set.new + end + + # Sequentially step through all of the available initialization routines, + # in order: + # + # * #set_load_path + # * #set_connection_adapters + # * #require_frameworks + # * #load_environment + # * #initialize_database + # * #initialize_logger + # * #initialize_framework_logging + # * #initialize_framework_views + # * #initialize_dependency_mechanism + # * #initialize_breakpoints + # * #initialize_whiny_nils + # * #initialize_framework_settings + # * #load_environment + # * #load_plugins + # * #initialize_routing + # + # (Note that #load_environment is invoked twice, once at the start and + # once at the end, to support the legacy configuration style where the + # environment could overwrite the defaults directly, instead of via the + # Configuration instance. + def process + check_ruby_version + set_load_path + set_connection_adapters + + require_frameworks + load_environment + + initialize_database + initialize_logger + initialize_framework_logging + initialize_framework_views + initialize_dependency_mechanism + initialize_breakpoints + initialize_whiny_nils + initialize_temporary_directories + + initialize_framework_settings + + # Support for legacy configuration style where the environment + # could overwrite anything set from the defaults/global through + # the individual base class configurations. + load_environment + + add_support_load_paths + + load_plugins + + # Routing must be initialized after plugins to allow the former to extend the routes + initialize_routing + + # the framework is now fully initialized + after_initialize + end + + # Check for valid Ruby version + # This is done in an external file, so we can use it + # from the `rails` program as well without duplication. + def check_ruby_version + require 'ruby_version_check' + end + + # Set the $LOAD_PATH based on the value of + # Configuration#load_paths. Duplicates are removed. + def set_load_path + configuration.load_paths.reverse.each { |dir| $LOAD_PATH.unshift(dir) if File.directory?(dir) } + $LOAD_PATH.uniq! + end + + # Sets the +RAILS_CONNECTION_ADAPTERS+ constant based on the value of + # Configuration#connection_adapters. This constant is used to determine + # which database adapters should be loaded (by default, all adapters are + # loaded). + def set_connection_adapters + Object.const_set("RAILS_CONNECTION_ADAPTERS", configuration.connection_adapters) if configuration.connection_adapters + end + + # Requires all frameworks specified by the Configuration#frameworks + # list. By default, all frameworks (ActiveRecord, ActiveSupport, + # ActionPack, ActionMailer, and ActionWebService) are loaded. + def require_frameworks + configuration.frameworks.each { |framework| require(framework.to_s) } + end + + # Add the load paths used by support functions such as the info controller + def add_support_load_paths + builtins = File.join(File.dirname(File.dirname(__FILE__)), 'builtin', '*') + $LOAD_PATH.concat(Dir[builtins]) + end + + # Loads all plugins in config.plugin_paths. plugin_paths + # defaults to vendor/plugins but may also be set to a list of + # paths, such as + # config.plugin_paths = ['lib/plugins', 'vendor/plugins'] + # + # Each plugin discovered in plugin_paths is initialized: + # * add its +lib+ directory, if present, to the beginning of the load path + # * evaluate init.rb if present + # + # After all plugins are loaded, duplicates are removed from the load path. + # Plugins are loaded in alphabetical order. + def load_plugins + find_plugins(configuration.plugin_paths).sort.each { |path| load_plugin path } + $LOAD_PATH.uniq! + end + + # Loads the environment specified by Configuration#environment_path, which + # is typically one of development, testing, or production. + def load_environment + silence_warnings do + config = configuration + constants = self.class.constants + eval(IO.read(configuration.environment_path), binding) + (self.class.constants - constants).each do |const| + Object.const_set(const, self.class.const_get(const)) + end + end + end + + # This initialization routine does nothing unless :active_record + # is one of the frameworks to load (Configuration#frameworks). If it is, + # this sets the database configuration from Configuration#database_configuration + # and then establishes the connection. + def initialize_database + return unless configuration.frameworks.include?(:active_record) + ActiveRecord::Base.configurations = configuration.database_configuration + ActiveRecord::Base.establish_connection + end + + # If the +RAILS_DEFAULT_LOGGER+ constant is already set, this initialization + # routine does nothing. If the constant is not set, and Configuration#logger + # is not +nil+, this also does nothing. Otherwise, a new logger instance + # is created at Configuration#log_path, with a default log level of + # Configuration#log_level. + # + # If the log could not be created, the log will be set to output to + # +STDERR+, with a log level of +WARN+. + def initialize_logger + # if the environment has explicitly defined a logger, use it + return if defined?(RAILS_DEFAULT_LOGGER) + + unless logger = configuration.logger + begin + logger = Logger.new(configuration.log_path) + logger.level = Logger.const_get(configuration.log_level.to_s.upcase) + rescue StandardError + logger = Logger.new(STDERR) + logger.level = Logger::WARN + logger.warn( + "Rails Error: Unable to access log file. Please ensure that #{configuration.log_path} exists and is chmod 0666. " + + "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed." + ) + end + end + + silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger } + end + + # Sets the logger for ActiveRecord, ActionController, and ActionMailer + # (but only for those frameworks that are to be loaded). If the framework's + # logger is already set, it is not changed, otherwise it is set to use + # +RAILS_DEFAULT_LOGGER+. + def initialize_framework_logging + for framework in ([ :active_record, :action_controller, :action_mailer ] & configuration.frameworks) + framework.to_s.camelize.constantize.const_get("Base").logger ||= RAILS_DEFAULT_LOGGER + end + end + + # Sets the +template_root+ for ActionController::Base and ActionMailer::Base + # (but only for those frameworks that are to be loaded). If the framework's + # +template_root+ has already been set, it is not changed, otherwise it is + # set to use Configuration#view_path. + def initialize_framework_views + for framework in ([ :action_controller, :action_mailer ] & configuration.frameworks) + framework.to_s.camelize.constantize.const_get("Base").template_root ||= configuration.view_path + end + end + + # If ActionController is not one of the loaded frameworks (Configuration#frameworks) + # this does nothing. Otherwise, it loads the routing definitions and sets up + # loading module used to lazily load controllers (Configuration#controller_paths). + def initialize_routing + return unless configuration.frameworks.include?(:action_controller) + ActionController::Routing::Routes.reload + end + + # Sets the dependency loading mechanism based on the value of + # Configuration#cache_classes. + def initialize_dependency_mechanism + Dependencies.mechanism = configuration.cache_classes ? :require : :load + end + + # Sets the +BREAKPOINT_SERVER_PORT+ if Configuration#breakpoint_server + # is true. + def initialize_breakpoints + silence_warnings { Object.const_set("BREAKPOINT_SERVER_PORT", 42531) if configuration.breakpoint_server } + end + + # Loads support for "whiny nil" (noisy warnings when methods are invoked + # on +nil+ values) if Configuration#whiny_nils is true. + def initialize_whiny_nils + require('active_support/whiny_nil') if configuration.whiny_nils + end + + def initialize_temporary_directories + if configuration.frameworks.include?(:action_controller) + session_path = "#{RAILS_ROOT}/tmp/sessions/" + ActionController::Base.session_options[:tmpdir] = File.exist?(session_path) ? session_path : Dir::tmpdir + + cache_path = "#{RAILS_ROOT}/tmp/cache/" + if File.exist?(cache_path) + ActionController::Base.fragment_cache_store = :file_store, cache_path + end + end + end + + # Initializes framework-specific settings for each of the loaded frameworks + # (Configuration#frameworks). The available settings map to the accessors + # on each of the corresponding Base classes. + def initialize_framework_settings + configuration.frameworks.each do |framework| + base_class = framework.to_s.camelize.constantize.const_get("Base") + + configuration.send(framework).each do |setting, value| + base_class.send("#{setting}=", value) + end + end + end + + # Fires the user-supplied after_initialize block (Configuration#after_initialize) + def after_initialize + configuration.after_initialize_block.call if configuration.after_initialize_block + end + + + protected + # Return a list of plugin paths within base_path. A plugin path is + # a directory that contains either a lib directory or an init.rb file. + # This recurses into directories which are not plugin paths, so you + # may organize your plugins within the plugin path. + def find_plugins(*base_paths) + base_paths.flatten.inject([]) do |plugins, base_path| + Dir.glob(File.join(base_path, '*')).each do |path| + if plugin_path?(path) + plugins << path + elsif File.directory?(path) + plugins += find_plugins(path) + end + end + plugins + end + end + + def plugin_path?(path) + File.directory?(path) and (File.directory?(File.join(path, 'lib')) or File.file?(File.join(path, 'init.rb'))) + end + + # Load the plugin at path unless already loaded. + # + # Each plugin is initialized: + # * add its +lib+ directory, if present, to the beginning of the load path + # * evaluate init.rb if present + # + # Returns true if the plugin is successfully loaded or + # false if it is already loaded (similar to Kernel#require). + # Raises LoadError if the plugin is not found. + def load_plugin(directory) + name = File.basename(directory) + return false if loaded_plugins.include?(name) + + # Catch nonexistent and empty plugins. + raise LoadError, "No such plugin: #{directory}" unless plugin_path?(directory) + + lib_path = File.join(directory, 'lib') + init_path = File.join(directory, 'init.rb') + has_lib = File.directory?(lib_path) + has_init = File.file?(init_path) + + # Add lib to load path *after* the application lib, to allow + # application libraries to override plugin libraries. + if has_lib + application_lib_index = $LOAD_PATH.index(File.join(RAILS_ROOT, "lib")) || 0 + $LOAD_PATH.insert(application_lib_index + 1, lib_path) + end + + # Allow plugins to reference the current configuration object + config = configuration + + # Add to set of loaded plugins before 'name' collapsed in eval. + loaded_plugins << name + + # Evaluate init.rb. + silence_warnings { eval(IO.read(init_path), binding, init_path) } if has_init + + true + end + end + + # The Configuration class holds all the parameters for the Initializer and + # ships with defaults that suites most Rails applications. But it's possible + # to overwrite everything. Usually, you'll create an Configuration file + # implicitly through the block running on the Initializer, but it's also + # possible to create the Configuration instance in advance and pass it in + # like this: + # + # config = Rails::Configuration.new + # Rails::Initializer.run(:process, config) + class Configuration + # A stub for setting options on ActionController::Base + attr_accessor :action_controller + + # A stub for setting options on ActionMailer::Base + attr_accessor :action_mailer + + # A stub for setting options on ActionView::Base + attr_accessor :action_view + + # A stub for setting options on ActionWebService::Base + attr_accessor :action_web_service + + # A stub for setting options on ActiveRecord::Base + attr_accessor :active_record + + # Whether or not to use the breakpoint server (boolean) + attr_accessor :breakpoint_server + + # Whether or not classes should be cached (set to false if you want + # application classes to be reloaded on each request) + attr_accessor :cache_classes + + # The list of connection adapters to load. (By default, all connection + # adapters are loaded. You can set this to be just the adapter(s) you + # will use to reduce your application's load time.) + attr_accessor :connection_adapters + + # The list of paths that should be searched for controllers. (Defaults + # to app/controllers and components.) + attr_accessor :controller_paths + + # The path to the database configuration file to use. (Defaults to + # config/database.yml.) + attr_accessor :database_configuration_file + + # The list of rails framework components that should be loaded. (Defaults + # to :active_record, :action_controller, + # :action_view, :action_mailer, and + # :action_web_service). + attr_accessor :frameworks + + # An array of additional paths to prepend to the load path. By default, + # all +app+, +lib+, +vendor+ and mock paths are included in this list. + attr_accessor :load_paths + + # The log level to use for the default Rails logger. In production mode, + # this defaults to :info. In development mode, it defaults to + # :debug. + attr_accessor :log_level + + # The path to the log file to use. Defaults to log/#{environment}.log + # (e.g. log/development.log or log/production.log). + attr_accessor :log_path + + # The specific logger to use. By default, a logger will be created and + # initialized using #log_path and #log_level, but a programmer may + # specifically set the logger to use via this accessor and it will be + # used directly. + attr_accessor :logger + + # The root of the application's views. (Defaults to app/views.) + attr_accessor :view_path + + # Set to +true+ if you want to be warned (noisily) when you try to invoke + # any method of +nil+. Set to +false+ for the standard Ruby behavior. + attr_accessor :whiny_nils + + # The path to the root of the plugins directory. By default, it is in + # vendor/plugins. + attr_accessor :plugin_paths + + # Create a new Configuration instance, initialized with the default + # values. + def initialize + self.frameworks = default_frameworks + self.load_paths = default_load_paths + self.log_path = default_log_path + self.log_level = default_log_level + self.view_path = default_view_path + self.controller_paths = default_controller_paths + self.cache_classes = default_cache_classes + self.breakpoint_server = default_breakpoint_server + self.whiny_nils = default_whiny_nils + self.plugin_paths = default_plugin_paths + self.database_configuration_file = default_database_configuration_file + + for framework in default_frameworks + self.send("#{framework}=", OrderedOptions.new) + end + end + + # Loads and returns the contents of the #database_configuration_file. The + # contents of the file are processed via ERB before being sent through + # YAML::load. + def database_configuration + YAML::load(ERB.new(IO.read(database_configuration_file)).result) + end + + # The path to the current environment's file (development.rb, etc.). By + # default the file is at config/environments/#{environment}.rb. + def environment_path + "#{root_path}/config/environments/#{environment}.rb" + end + + # Return the currently selected environment. By default, it returns the + # value of the +RAILS_ENV+ constant. + def environment + ::RAILS_ENV + end + + # Sets a block which will be executed after rails has been fully initialized. + # Useful for per-environment configuration which depends on the framework being + # fully initialized. + def after_initialize(&after_initialize_block) + @after_initialize_block = after_initialize_block + end + + # Returns the block set in Configuration#after_initialize + def after_initialize_block + @after_initialize_block + end + + private + def root_path + ::RAILS_ROOT + end + + def framework_root_path + defined?(::RAILS_FRAMEWORK_ROOT) ? ::RAILS_FRAMEWORK_ROOT : "#{root_path}/vendor/rails" + end + + def default_frameworks + [ :active_record, :action_controller, :action_view, :action_mailer, :action_web_service ] + end + + def default_load_paths + paths = ["#{root_path}/test/mocks/#{environment}"] + + # Add the app's controller directory + paths.concat(Dir["#{root_path}/app/controllers/"]) + + # Then model subdirectories. + # TODO: Don't include .rb models as load paths + paths.concat(Dir["#{root_path}/app/models/[_a-z]*"]) + paths.concat(Dir["#{root_path}/components/[_a-z]*"]) + + # Followed by the standard includes. + paths.concat %w( + app + app/models + app/controllers + app/helpers + app/services + app/apis + components + config + lib + vendor + ).map { |dir| "#{root_path}/#{dir}" }.select { |dir| File.directory?(dir) } + + # TODO: Don't include dirs for frameworks that are not used + paths.concat %w( + railties + railties/lib + actionpack/lib + activesupport/lib + activerecord/lib + actionmailer/lib + actionwebservice/lib + ).map { |dir| "#{framework_root_path}/#{dir}" }.select { |dir| File.directory?(dir) } + end + + def default_log_path + File.join(root_path, 'log', "#{environment}.log") + end + + def default_log_level + environment == 'production' ? :info : :debug + end + + def default_database_configuration_file + File.join(root_path, 'config', 'database.yml') + end + + def default_view_path + File.join(root_path, 'app', 'views') + end + + def default_controller_paths + [ File.join(root_path, 'app', 'controllers'), File.join(root_path, 'components'), File.join(RAILTIES_PATH, 'builtin', 'controllers') ] + end + + def default_dependency_mechanism + :load + end + + def default_cache_classes + false + end + + def default_breakpoint_server + false + end + + def default_whiny_nils + false + end + + def default_plugin_paths + ["#{root_path}/vendor/plugins"] + end + end +end + +# Needs to be duplicated from Active Support since its needed before Active +# Support is available. +class OrderedHash < Array #:nodoc: + def []=(key, value) + if pair = find_pair(key) + pair.pop + pair << value + else + self << [key, value] + end + end + + def [](key) + pair = find_pair(key) + pair ? pair.last : nil + end + + def keys + self.collect { |i| i.first } + end + + private + def find_pair(key) + self.each { |i| return i if i.first == key } + return false + end +end + +class OrderedOptions < OrderedHash #:nodoc: + def []=(key, value) + super(key.to_sym, value) + end + + def [](key) + super(key.to_sym) + end + + def method_missing(name, *args) + if name.to_s =~ /(.*)=$/ + self[$1.to_sym] = args.first + else + self[name] + end + end +end \ No newline at end of file diff --git a/vendor/rails/railties/lib/rails/version.rb b/vendor/rails/railties/lib/rails/version.rb new file mode 100644 index 00000000..3bad3f04 --- /dev/null +++ b/vendor/rails/railties/lib/rails/version.rb @@ -0,0 +1,9 @@ +module Rails + module VERSION #:nodoc: + MAJOR = 1 + MINOR = 1 + TINY = 6 + + STRING = [MAJOR, MINOR, TINY].join('.') + end +end diff --git a/vendor/rails/railties/lib/rails_generator.rb b/vendor/rails/railties/lib/rails_generator.rb new file mode 100644 index 00000000..9c587c95 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator.rb @@ -0,0 +1,43 @@ +#-- +# Copyright (c) 2004 Jeremy Kemper +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +$:.unshift(File.dirname(__FILE__)) +$:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib") + +begin + require 'active_support' +rescue LoadError + require 'rubygems' + require_gem 'activesupport' +end + +require 'rails_generator/base' +require 'rails_generator/lookup' +require 'rails_generator/commands' + +Rails::Generator::Base.send(:include, Rails::Generator::Lookup) +Rails::Generator::Base.send(:include, Rails::Generator::Commands) + +# Set up a default logger for convenience. +require 'rails_generator/simple_logger' +Rails::Generator::Base.logger = Rails::Generator::SimpleLogger.new(STDOUT) diff --git a/vendor/rails/railties/lib/rails_generator/base.rb b/vendor/rails/railties/lib/rails_generator/base.rb new file mode 100644 index 00000000..6a56f96b --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/base.rb @@ -0,0 +1,203 @@ +require File.dirname(__FILE__) + '/options' +require File.dirname(__FILE__) + '/manifest' +require File.dirname(__FILE__) + '/spec' + +# Rails::Generator is a code generation platform tailored for the Rails +# web application framework. Generators are easily invoked within Rails +# applications to add and remove components such as models and controllers. +# New generators are easy to create and may be distributed as RubyGems or +# tarballs for inclusion system-wide, per-user, or per-application. +# +# Generators may subclass other generators to provide variations that +# require little or no new logic but replace the template files. +# The postback generator is an example: it subclasses the scaffold +# generator and just replaces the code templates with its own. +# +# Now go forth and multiply^Wgenerate. +module Rails + module Generator + class GeneratorError < StandardError; end + class UsageError < GeneratorError; end + + + # The base code generator is bare-bones. It sets up the source and + # destination paths and tells the logger whether to keep its trap shut. + # You're probably looking for NamedBase, a subclass meant for generating + # "named" components such as models, controllers, and mailers. + # + # Generators create a manifest of the actions they perform then hand + # the manifest to a command which replay the actions to do the heavy + # lifting. Create, destroy, and list commands are included. Since a + # single manifest may be used by any command, creating new generators is + # as simple as writing some code templates and declaring what you'd like + # to do with them. + # + # The manifest method must be implemented by subclasses, returning a + # Rails::Generator::Manifest. The record method is provided as a + # convenience for manifest creation. Example: + # class EliteGenerator < Rails::Generator::Base + # def manifest + # record do |m| + # m.do(some) + # m.things(in) { here } + # end + # end + # end + class Base + include Options + + # Declare default options for the generator. These options + # are inherited to subclasses. + default_options :collision => :ask, :quiet => false + + # A logger instance available everywhere in the generator. + cattr_accessor :logger + + # Every generator that is dynamically looked up is tagged with a + # Spec describing where it was found. + class_inheritable_accessor :spec + + attr_reader :source_root, :destination_root, :args + + def initialize(runtime_args, runtime_options = {}) + @args = runtime_args + parse!(@args, runtime_options) + + # Derive source and destination paths. + @source_root = options[:source] || File.join(spec.path, 'templates') + if options[:destination] + @destination_root = options[:destination] + elsif defined? ::RAILS_ROOT + @destination_root = ::RAILS_ROOT + end + + # Silence the logger if requested. + logger.quiet = options[:quiet] + + # Raise usage error if help is requested. + usage if options[:help] + end + + # Generators must provide a manifest. Use the record method to create + # a new manifest and record your generator's actions. + def manifest + raise NotImplementedError, "No manifest for '#{spec.name}' generator." + end + + # Return the full path from the source root for the given path. + # Example for source_root = '/source': + # source_path('some/path.rb') == '/source/some/path.rb' + # + # The given path may include a colon ':' character to indicate that + # the file belongs to another generator. This notation allows any + # generator to borrow files from another. Example: + # source_path('model:fixture.yml') = '/model/source/path/fixture.yml' + def source_path(relative_source) + # Check whether we're referring to another generator's file. + name, path = relative_source.split(':', 2) + + # If not, return the full path to our source file. + if path.nil? + File.join(source_root, name) + + # Otherwise, ask our referral for the file. + else + # FIXME: this is broken, though almost always true. Others' + # source_root are not necessarily the templates dir. + File.join(self.class.lookup(name).path, 'templates', path) + end + end + + # Return the full path from the destination root for the given path. + # Example for destination_root = '/dest': + # destination_path('some/path.rb') == '/dest/some/path.rb' + def destination_path(relative_destination) + File.join(destination_root, relative_destination) + end + + protected + # Convenience method for generator subclasses to record a manifest. + def record + Rails::Generator::Manifest.new(self) { |m| yield m } + end + + # Override with your own usage banner. + def banner + "Usage: #{$0} #{spec.name} [options]" + end + + # Read USAGE from file in generator base path. + def usage_message + File.read(File.join(spec.path, 'USAGE')) rescue '' + end + end + + + # The base generator for named components: models, controllers, mailers, + # etc. The target name is taken as the first argument and inflected to + # singular, plural, class, file, and table forms for your convenience. + # The remaining arguments are aliased to actions for controller and + # mailer convenience. + # + # If no name is provided, the generator raises a usage error with content + # optionally read from the USAGE file in the generator's base path. + # + # See Rails::Generator::Base for a discussion of Manifests and Commands. + class NamedBase < Base + attr_reader :name, :class_name, :singular_name, :plural_name, :table_name + attr_reader :class_path, :file_path, :class_nesting, :class_nesting_depth + alias_method :file_name, :singular_name + alias_method :actions, :args + + def initialize(runtime_args, runtime_options = {}) + super + + # Name argument is required. + usage if runtime_args.empty? + + @args = runtime_args.dup + base_name = @args.shift + assign_names!(base_name) + end + + protected + # Override with your own usage banner. + def banner + "Usage: #{$0} #{spec.name} #{spec.name.camelize}Name [options]" + end + + private + def assign_names!(name) + @name = name + base_name, @class_path, @file_path, @class_nesting, @class_nesting_depth = extract_modules(@name) + @class_name_without_nesting, @singular_name, @plural_name = inflect_names(base_name) + @table_name = ActiveRecord::Base.pluralize_table_names ? plural_name : singular_name + if @class_nesting.empty? + @class_name = @class_name_without_nesting + else + @class_name = "#{@class_nesting}::#{@class_name_without_nesting}" + end + end + + # Extract modules from filesystem-style or ruby-style path: + # good/fun/stuff + # Good::Fun::Stuff + # produce the same results. + def extract_modules(name) + modules = name.include?('/') ? name.split('/') : name.split('::') + name = modules.pop + path = modules.map { |m| m.underscore } + file_path = (path + [name.underscore]).join('/') + nesting = modules.map { |m| m.camelize }.join('::') + [name, path, file_path, nesting, modules.size] + end + + def inflect_names(name) + camel = name.camelize + under = camel.underscore + plural = under.pluralize + [camel, under, plural] + end + end + end +end diff --git a/vendor/rails/railties/lib/rails_generator/commands.rb b/vendor/rails/railties/lib/rails_generator/commands.rb new file mode 100644 index 00000000..8d0ac52b --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/commands.rb @@ -0,0 +1,519 @@ +require 'delegate' +require 'optparse' +require 'fileutils' +require 'erb' + +module Rails + module Generator + module Commands + # Here's a convenient way to get a handle on generator commands. + # Command.instance('destroy', my_generator) instantiates a Destroy + # delegate of my_generator ready to do your dirty work. + def self.instance(command, generator) + const_get(command.to_s.camelize).new(generator) + end + + # Even more convenient access to commands. Include Commands in + # the generator Base class to get a nice #command instance method + # which returns a delegate for the requested command. + def self.append_features(base) + base.send(:define_method, :command) do |command| + Commands.instance(command, self) + end + end + + + # Generator commands delegate Rails::Generator::Base and implement + # a standard set of actions. Their behavior is defined by the way + # they respond to these actions: Create brings life; Destroy brings + # death; List passively observes. + # + # Commands are invoked by replaying (or rewinding) the generator's + # manifest of actions. See Rails::Generator::Manifest and + # Rails::Generator::Base#manifest method that generator subclasses + # are required to override. + # + # Commands allows generators to "plug in" invocation behavior, which + # corresponds to the GoF Strategy pattern. + class Base < DelegateClass(Rails::Generator::Base) + # Replay action manifest. RewindBase subclass rewinds manifest. + def invoke! + manifest.replay(self) + end + + def dependency(generator_name, args, runtime_options = {}) + logger.dependency(generator_name) do + self.class.new(instance(generator_name, args, full_options(runtime_options))).invoke! + end + end + + # Does nothing for all commands except Create. + def class_collisions(*class_names) + end + + # Does nothing for all commands except Create. + def readme(*args) + end + + protected + def migration_directory(relative_path) + directory(@migration_directory = relative_path) + end + + def existing_migrations(file_name) + Dir.glob("#{@migration_directory}/[0-9]*_*.rb").grep(/[0-9]+_#{file_name}.rb$/) + end + + def migration_exists?(file_name) + not existing_migrations(file_name).empty? + end + + def current_migration_number + Dir.glob("#{@migration_directory}/[0-9]*.rb").inject(0) do |max, file_path| + n = File.basename(file_path).split('_', 2).first.to_i + if n > max then n else max end + end + end + + def next_migration_number + current_migration_number + 1 + end + + def next_migration_string(padding = 3) + "%.#{padding}d" % next_migration_number + end + + private + # Ask the user interactively whether to force collision. + def force_file_collision?(destination) + $stdout.print "overwrite #{destination}? [Ynaq] " + case $stdin.gets + when /a/i + $stdout.puts "forcing #{spec.name}" + options[:collision] = :force + when /q/i + $stdout.puts "aborting #{spec.name}" + raise SystemExit + when /n/i then :skip + else :force + end + rescue + retry + end + + def render_template_part(template_options) + # Getting Sandbox to evaluate part template in it + part_binding = template_options[:sandbox].call.sandbox_binding + part_rel_path = template_options[:insert] + part_path = source_path(part_rel_path) + + # Render inner template within Sandbox binding + rendered_part = ERB.new(File.readlines(part_path).join, nil, '-').result(part_binding) + begin_mark = template_part_mark(template_options[:begin_mark], template_options[:mark_id]) + end_mark = template_part_mark(template_options[:end_mark], template_options[:mark_id]) + begin_mark + rendered_part + end_mark + end + + def template_part_mark(name, id) + "\n" + end + end + + # Base class for commands which handle generator actions in reverse, such as Destroy. + class RewindBase < Base + # Rewind action manifest. + def invoke! + manifest.rewind(self) + end + end + + + # Create is the premier generator command. It copies files, creates + # directories, renders templates, and more. + class Create < Base + + # Check whether the given class names are already taken by + # Ruby or Rails. In the future, expand to check other namespaces + # such as the rest of the user's app. + def class_collisions(*class_names) + class_names.flatten.each do |class_name| + # Convert to string to allow symbol arguments. + class_name = class_name.to_s + + # Skip empty strings. + next if class_name.strip.empty? + + # Split the class from its module nesting. + nesting = class_name.split('::') + name = nesting.pop + + # Extract the last Module in the nesting. + last = nesting.inject(Object) { |last, nest| + break unless last.const_defined?(nest) + last.const_get(nest) + } + + # If the last Module exists, check whether the given + # class exists and raise a collision if so. + if last and last.const_defined?(name.camelize) + raise_class_collision(class_name) + end + end + end + + # Copy a file from source to destination with collision checking. + # + # The file_options hash accepts :chmod and :shebang and :collision options. + # :chmod sets the permissions of the destination file: + # file 'config/empty.log', 'log/test.log', :chmod => 0664 + # :shebang sets the #!/usr/bin/ruby line for scripts + # file 'bin/generate.rb', 'script/generate', :chmod => 0755, :shebang => '/usr/bin/env ruby' + # :collision sets the collision option only for the destination file: + # file 'settings/server.yml', 'config/server.yml', :collision => :skip + # + # Collisions are handled by checking whether the destination file + # exists and either skipping the file, forcing overwrite, or asking + # the user what to do. + def file(relative_source, relative_destination, file_options = {}, &block) + # Determine full paths for source and destination files. + source = source_path(relative_source) + destination = destination_path(relative_destination) + destination_exists = File.exists?(destination) + + # If source and destination are identical then we're done. + if destination_exists and identical?(source, destination, &block) + return logger.identical(relative_destination) + end + + # Check for and resolve file collisions. + if destination_exists + + # Make a choice whether to overwrite the file. :force and + # :skip already have their mind made up, but give :ask a shot. + choice = case (file_options[:collision] || options[:collision]).to_sym #|| :ask + when :ask then force_file_collision?(relative_destination) + when :force then :force + when :skip then :skip + else raise "Invalid collision option: #{options[:collision].inspect}" + end + + # Take action based on our choice. Bail out if we chose to + # skip the file; otherwise, log our transgression and continue. + case choice + when :force then logger.force(relative_destination) + when :skip then return(logger.skip(relative_destination)) + else raise "Invalid collision choice: #{choice}.inspect" + end + + # File doesn't exist so log its unbesmirched creation. + else + logger.create relative_destination + end + + # If we're pretending, back off now. + return if options[:pretend] + + # Write destination file with optional shebang. Yield for content + # if block given so templaters may render the source file. If a + # shebang is requested, replace the existing shebang or insert a + # new one. + File.open(destination, 'wb') do |df| + File.open(source, 'rb') do |sf| + if block_given? + df.write(yield(sf)) + else + if file_options[:shebang] + df.puts("#!#{file_options[:shebang]}") + if line = sf.gets + df.puts(line) if line !~ /^#!/ + end + end + df.write(sf.read) + end + end + end + + # Optionally change permissions. + if file_options[:chmod] + FileUtils.chmod(file_options[:chmod], destination) + end + + # Optionally add file to subversion + system("svn add #{destination}") if options[:svn] + end + + # Checks if the source and the destination file are identical. If + # passed a block then the source file is a template that needs to first + # be evaluated before being compared to the destination. + def identical?(source, destination, &block) + return false if File.directory? destination + source = block_given? ? File.open(source) {|sf| yield(sf)} : IO.read(source) + destination = IO.read(destination) + source == destination + end + + # Generate a file for a Rails application using an ERuby template. + # Looks up and evalutes a template by name and writes the result. + # + # The ERB template uses explicit trim mode to best control the + # proliferation of whitespace in generated code. <%- trims leading + # whitespace; -%> trims trailing whitespace including one newline. + # + # A hash of template options may be passed as the last argument. + # The options accepted by the file are accepted as well as :assigns, + # a hash of variable bindings. Example: + # template 'foo', 'bar', :assigns => { :action => 'view' } + # + # Template is implemented in terms of file. It calls file with a + # block which takes a file handle and returns its rendered contents. + def template(relative_source, relative_destination, template_options = {}) + file(relative_source, relative_destination, template_options) do |file| + # Evaluate any assignments in a temporary, throwaway binding. + vars = template_options[:assigns] || {} + b = binding + vars.each { |k,v| eval "#{k} = vars[:#{k}] || vars['#{k}']", b } + + # Render the source file with the temporary binding. + ERB.new(file.read, nil, '-').result(b) + end + end + + def complex_template(relative_source, relative_destination, template_options = {}) + options = template_options.dup + options[:assigns] ||= {} + options[:assigns]['template_for_inclusion'] = render_template_part(template_options) + template(relative_source, relative_destination, options) + end + + # Create a directory including any missing parent directories. + # Always directories which exist. + def directory(relative_path) + path = destination_path(relative_path) + if File.exists?(path) + logger.exists relative_path + else + logger.create relative_path + FileUtils.mkdir_p(path) unless options[:pretend] + + # Optionally add file to subversion + system("svn add #{path}") if options[:svn] + end + end + + # Display a README. + def readme(*relative_sources) + relative_sources.flatten.each do |relative_source| + logger.readme relative_source + puts File.read(source_path(relative_source)) unless options[:pretend] + end + end + + # When creating a migration, it knows to find the first available file in db/migrate and use the migration.rb template. + def migration_template(relative_source, relative_destination, template_options = {}) + migration_directory relative_destination + migration_file_name = template_options[:migration_file_name] || file_name + raise "Another migration is already named #{migration_file_name}: #{existing_migrations(migration_file_name).first}" if migration_exists?(migration_file_name) + template(relative_source, "#{relative_destination}/#{next_migration_string}_#{migration_file_name}.rb", template_options) + end + + private + # Raise a usage error with an informative WordNet suggestion. + # Thanks to Florian Gross (flgr). + def raise_class_collision(class_name) + message = <", "") + data.scan(/^Sense \d+\n.+?\n\n/m) + end + end + rescue Exception + return nil + end + end + + + # Undo the actions performed by a generator. Rewind the action + # manifest and attempt to completely erase the results of each action. + class Destroy < RewindBase + # Remove a file if it exists and is a file. + def file(relative_source, relative_destination, file_options = {}) + destination = destination_path(relative_destination) + if File.exists?(destination) + logger.rm relative_destination + unless options[:pretend] + if options[:svn] + # If the file has been marked to be added + # but has not yet been checked in, revert and delete + if options[:svn][relative_destination] + system("svn revert #{destination}") + FileUtils.rm(destination) + else + # If the directory is not in the status list, it + # has no modifications so we can simply remove it + system("svn rm #{destination}") + end + else + FileUtils.rm(destination) + end + end + else + logger.missing relative_destination + return + end + end + + # Templates are deleted just like files and the actions take the + # same parameters, so simply alias the file method. + alias_method :template, :file + + # Remove each directory in the given path from right to left. + # Remove each subdirectory if it exists and is a directory. + def directory(relative_path) + parts = relative_path.split('/') + until parts.empty? + partial = File.join(parts) + path = destination_path(partial) + if File.exists?(path) + if Dir[File.join(path, '*')].empty? + logger.rmdir partial + unless options[:pretend] + if options[:svn] + # If the directory has been marked to be added + # but has not yet been checked in, revert and delete + if options[:svn][relative_path] + system("svn revert #{path}") + FileUtils.rmdir(path) + else + # If the directory is not in the status list, it + # has no modifications so we can simply remove it + system("svn rm #{path}") + end + else + FileUtils.rmdir(path) + end + end + else + logger.notempty partial + end + else + logger.missing partial + end + parts.pop + end + end + + def complex_template(*args) + # nothing should be done here + end + + # When deleting a migration, it knows to delete every file named "[0-9]*_#{file_name}". + def migration_template(relative_source, relative_destination, template_options = {}) + migration_directory relative_destination + + migration_file_name = template_options[:migration_file_name] || file_name + unless migration_exists?(migration_file_name) + puts "There is no migration named #{migration_file_name}" + return + end + + + existing_migrations(migration_file_name).each do |file_path| + file(relative_source, file_path, template_options) + end + end + end + + + # List a generator's action manifest. + class List < Base + def dependency(generator_name, args, options = {}) + logger.dependency "#{generator_name}(#{args.join(', ')}, #{options.inspect})" + end + + def class_collisions(*class_names) + logger.class_collisions class_names.join(', ') + end + + def file(relative_source, relative_destination, options = {}) + logger.file relative_destination + end + + def template(relative_source, relative_destination, options = {}) + logger.template relative_destination + end + + def complex_template(relative_source, relative_destination, options = {}) + logger.template "#{options[:insert]} inside #{relative_destination}" + end + + def directory(relative_path) + logger.directory "#{destination_path(relative_path)}/" + end + + def readme(*args) + logger.readme args.join(', ') + end + + def migration_template(relative_source, relative_destination, options = {}) + migration_directory relative_destination + logger.migration_template file_name + end + end + + # Update generator's action manifest. + class Update < Create + def file(relative_source, relative_destination, options = {}) + # logger.file relative_destination + end + + def template(relative_source, relative_destination, options = {}) + # logger.template relative_destination + end + + def complex_template(relative_source, relative_destination, template_options = {}) + + begin + dest_file = destination_path(relative_destination) + source_to_update = File.readlines(dest_file).join + rescue Errno::ENOENT + logger.missing relative_destination + return + end + + logger.refreshing "#{template_options[:insert].gsub(/\.rhtml/,'')} inside #{relative_destination}" + + begin_mark = Regexp.quote(template_part_mark(template_options[:begin_mark], template_options[:mark_id])) + end_mark = Regexp.quote(template_part_mark(template_options[:end_mark], template_options[:mark_id])) + + # Refreshing inner part of the template with freshly rendered part. + rendered_part = render_template_part(template_options) + source_to_update.gsub!(/#{begin_mark}.*?#{end_mark}/m, rendered_part) + + File.open(dest_file, 'w') { |file| file.write(source_to_update) } + end + + def directory(relative_path) + # logger.directory "#{destination_path(relative_path)}/" + end + end + + end + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/applications/app/USAGE b/vendor/rails/railties/lib/rails_generator/generators/applications/app/USAGE new file mode 100644 index 00000000..3bb55113 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/applications/app/USAGE @@ -0,0 +1,16 @@ +Description: + The 'rails' command creates a new Rails application with a default + directory structure and configuration at the path you specify. + +Example: + rails ~/Code/Ruby/weblog + + This generates a skeletal Rails installation in ~/Code/Ruby/weblog. + See the README in the newly created application to get going. + +WARNING: + Only specify --without-gems if you did not use gems to install Rails. + Your application will expect to find activerecord, actionpack, and + actionmailer directories in the vendor directory. A popular way to track + the bleeding edge of Rails development is to checkout from source control + directly to the vendor directory. See http://dev.rubyonrails.com diff --git a/vendor/rails/railties/lib/rails_generator/generators/applications/app/app_generator.rb b/vendor/rails/railties/lib/rails_generator/generators/applications/app/app_generator.rb new file mode 100644 index 00000000..d59c1d05 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/applications/app/app_generator.rb @@ -0,0 +1,157 @@ +require 'rbconfig' + +class AppGenerator < Rails::Generator::Base + DEFAULT_SHEBANG = File.join(Config::CONFIG['bindir'], + Config::CONFIG['ruby_install_name']) + + DATABASES = %w( mysql oracle postgresql sqlite2 sqlite3 ) + + default_options :db => "mysql", :shebang => DEFAULT_SHEBANG, :freeze => false + mandatory_options :source => "#{File.dirname(__FILE__)}/../../../../.." + + def initialize(runtime_args, runtime_options = {}) + super + usage if args.empty? + usage("Databases supported for preconfiguration are: #{DATABASES.join(", ")}") if (options[:db] && !DATABASES.include?(options[:db])) + @destination_root = args.shift + end + + def manifest + # Use /usr/bin/env if no special shebang was specified + script_options = { :chmod => 0755, :shebang => options[:shebang] == DEFAULT_SHEBANG ? nil : options[:shebang] } + dispatcher_options = { :chmod => 0755, :shebang => options[:shebang] } + + record do |m| + # Root directory and all subdirectories. + m.directory '' + BASEDIRS.each { |path| m.directory path } + + # Root + m.file "fresh_rakefile", "Rakefile" + m.file "README", "README" + + # Application + m.template "helpers/application.rb", "app/controllers/application.rb" + m.template "helpers/application_helper.rb", "app/helpers/application_helper.rb" + m.template "helpers/test_helper.rb", "test/test_helper.rb" + + # database.yml and .htaccess + m.template "configs/databases/#{options[:db]}.yml", "config/database.yml", :assigns => { + :app_name => File.basename(File.expand_path(@destination_root)), + :socket => options[:db] == "mysql" ? mysql_socket_location : nil + } + m.template "configs/routes.rb", "config/routes.rb" + m.template "configs/apache.conf", "public/.htaccess" + + # Environments + m.file "environments/boot.rb", "config/boot.rb" + m.template "environments/environment.rb", "config/environment.rb", :assigns => { :freeze => options[:freeze] } + m.file "environments/production.rb", "config/environments/production.rb" + m.file "environments/development.rb", "config/environments/development.rb" + m.file "environments/test.rb", "config/environments/test.rb" + + # Scripts + %w( about breakpointer console destroy generate performance/benchmarker performance/profiler process/reaper process/spawner runner server plugin ).each do |file| + m.file "bin/#{file}", "script/#{file}", script_options + end + + # Dispatches + m.file "dispatches/dispatch.rb", "public/dispatch.rb", dispatcher_options + m.file "dispatches/dispatch.rb", "public/dispatch.cgi", dispatcher_options + m.file "dispatches/dispatch.fcgi", "public/dispatch.fcgi", dispatcher_options + + # HTML files + %w(404 500 index).each do |file| + m.template "html/#{file}.html", "public/#{file}.html" + end + + m.template "html/favicon.ico", "public/favicon.ico" + m.template "html/robots.txt", "public/robots.txt" + m.file "html/images/rails.png", "public/images/rails.png" + + # Javascripts + m.file "html/javascripts/prototype.js", "public/javascripts/prototype.js" + m.file "html/javascripts/effects.js", "public/javascripts/effects.js" + m.file "html/javascripts/dragdrop.js", "public/javascripts/dragdrop.js" + m.file "html/javascripts/controls.js", "public/javascripts/controls.js" + m.file "html/javascripts/application.js", "public/javascripts/application.js" + + # Docs + m.file "doc/README_FOR_APP", "doc/README_FOR_APP" + + # Logs + %w(server production development test).each { |file| + m.file "configs/empty.log", "log/#{file}.log", :chmod => 0666 + } + end + end + + protected + def banner + "Usage: #{$0} /path/to/your/app [options]" + end + + def add_options!(opt) + opt.separator '' + opt.separator 'Options:' + opt.on("-r", "--ruby=path", String, + "Path to the Ruby binary of your choice (otherwise scripts use env, dispatchers current path).", + "Default: #{DEFAULT_SHEBANG}") { |v| options[:shebang] = v } + + opt.on("-d", "--database=name", String, + "Preconfigure for selected database (options: mysql/oracle/postgresql/sqlite2/sqlite3).", + "Default: mysql") { |v| options[:db] = v } + + opt.on("-f", "--freeze", + "Freeze Rails in vendor/rails from the gems generating the skeleton", + "Default: false") { |v| options[:freeze] = v } + end + + def mysql_socket_location + RUBY_PLATFORM =~ /mswin32/ ? MYSQL_SOCKET_LOCATIONS.find { |f| File.exists?(f) } : nil + end + + + # Installation skeleton. Intermediate directories are automatically + # created so don't sweat their absence here. + BASEDIRS = %w( + app/controllers + app/helpers + app/models + app/views/layouts + config/environments + components + db + doc + lib + lib/tasks + log + public/images + public/javascripts + public/stylesheets + script/performance + script/process + test/fixtures + test/functional + test/integration + test/mocks/development + test/mocks/test + test/unit + vendor + vendor/plugins + tmp/sessions + tmp/sockets + tmp/cache + ) + + MYSQL_SOCKET_LOCATIONS = [ + "/tmp/mysql.sock", # default + "/var/run/mysqld/mysqld.sock", # debian/gentoo + "/var/tmp/mysql.sock", # freebsd + "/var/lib/mysql/mysql.sock", # fedora + "/opt/local/lib/mysql/mysql.sock", # fedora + "/opt/local/var/run/mysqld/mysqld.sock", # mac + darwinports + mysql + "/opt/local/var/run/mysql4/mysqld.sock", # mac + darwinports + mysql4 + "/opt/local/var/run/mysql5/mysqld.sock" # mac + darwinports + mysql5 + ] +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/controller/USAGE b/vendor/rails/railties/lib/rails_generator/generators/components/controller/USAGE new file mode 100644 index 00000000..ec642091 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/controller/USAGE @@ -0,0 +1,30 @@ +Description: + The controller generator creates stubs for a new controller and its views. + + The generator takes a controller name and a list of views as arguments. + The controller name may be given in CamelCase or under_score and should + not be suffixed with 'Controller'. To create a controller within a + module, specify the controller name as 'module/controller'. + + The generator creates a controller class in app/controllers with view + templates in app/views/controller_name, a helper class in app/helpers, + and a functional test suite in test/functional. + +Example: + ./script/generate controller CreditCard open debit credit close + + Credit card controller with URLs like /credit_card/debit. + Controller: app/controllers/credit_card_controller.rb + Views: app/views/credit_card/debit.rhtml [...] + Helper: app/helpers/credit_card_helper.rb + Test: test/functional/credit_card_controller_test.rb + +Modules Example: + ./script/generate controller 'admin/credit_card' suspend late_fee + + Credit card admin controller with URLs /admin/credit_card/suspend. + Controller: app/controllers/admin/credit_card_controller.rb + Views: app/views/admin/credit_card/debit.rhtml [...] + Helper: app/helpers/admin/credit_card_helper.rb + Test: test/functional/admin/credit_card_controller_test.rb + diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/controller/controller_generator.rb b/vendor/rails/railties/lib/rails_generator/generators/components/controller/controller_generator.rb new file mode 100644 index 00000000..358d3574 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/controller/controller_generator.rb @@ -0,0 +1,38 @@ +class ControllerGenerator < Rails::Generator::NamedBase + def manifest + record do |m| + # Check for class naming collisions. + m.class_collisions class_path, "#{class_name}Controller", "#{class_name}ControllerTest", "#{class_name}Helper" + + # Controller, helper, views, and test directories. + m.directory File.join('app/controllers', class_path) + m.directory File.join('app/helpers', class_path) + m.directory File.join('app/views', class_path, file_name) + m.directory File.join('test/functional', class_path) + + # Controller class, functional test, and helper class. + m.template 'controller.rb', + File.join('app/controllers', + class_path, + "#{file_name}_controller.rb") + + m.template 'functional_test.rb', + File.join('test/functional', + class_path, + "#{file_name}_controller_test.rb") + + m.template 'helper.rb', + File.join('app/helpers', + class_path, + "#{file_name}_helper.rb") + + # View template for each action. + actions.each do |action| + path = File.join('app/views', class_path, file_name, "#{action}.rhtml") + m.template 'view.rhtml', + path, + :assigns => { :action => action, :path => path } + end + end + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/controller.rb b/vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/controller.rb new file mode 100644 index 00000000..da71b5f0 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/controller.rb @@ -0,0 +1,10 @@ +class <%= class_name %>Controller < ApplicationController +<% if options[:scaffold] -%> + scaffold :<%= singular_name %> +<% end -%> +<% for action in actions -%> + + def <%= action %> + end +<% end -%> +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/functional_test.rb b/vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/functional_test.rb new file mode 100644 index 00000000..abe9c4cf --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/functional_test.rb @@ -0,0 +1,18 @@ +require File.dirname(__FILE__) + '<%= '/..' * class_nesting_depth %>/../test_helper' +require '<%= file_path %>_controller' + +# Re-raise errors caught by the controller. +class <%= class_name %>Controller; def rescue_action(e) raise e end; end + +class <%= class_name %>ControllerTest < Test::Unit::TestCase + def setup + @controller = <%= class_name %>Controller.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/helper.rb b/vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/helper.rb new file mode 100644 index 00000000..3fe2ecdc --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/helper.rb @@ -0,0 +1,2 @@ +module <%= class_name %>Helper +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/view.rhtml b/vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/view.rhtml new file mode 100644 index 00000000..ad85431f --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/view.rhtml @@ -0,0 +1,2 @@ +

      <%= class_name %>#<%= action %>

      +

      Find me in <%= path %>

      diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/integration_test/USAGE b/vendor/rails/railties/lib/rails_generator/generators/components/integration_test/USAGE new file mode 100644 index 00000000..d1ed71a4 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/integration_test/USAGE @@ -0,0 +1,14 @@ +Description: + The model generator creates a stub for a new integration test. + + The generator takes an integration test name as its argument. The test + name may be given in CamelCase or under_score and should not be suffixed + with 'Test'. + + The generator creates an integration test class in test/integration. + +Example: + ./script/generate integration_test GeneralStories + + This will create a GeneralStores integration test: + test/integration/general_stories_test.rb diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/integration_test/integration_test_generator.rb b/vendor/rails/railties/lib/rails_generator/generators/components/integration_test/integration_test_generator.rb new file mode 100644 index 00000000..90fa9693 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/integration_test/integration_test_generator.rb @@ -0,0 +1,16 @@ +class IntegrationTestGenerator < Rails::Generator::NamedBase + default_options :skip_migration => false + + def manifest + record do |m| + # Check for class naming collisions. + m.class_collisions class_path, class_name, "#{class_name}Test" + + # integration test directory + m.directory File.join('test/integration', class_path) + + # integration test stub + m.template 'integration_test.rb', File.join('test/integration', class_path, "#{file_name}_test.rb") + end + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/integration_test/templates/integration_test.rb b/vendor/rails/railties/lib/rails_generator/generators/components/integration_test/templates/integration_test.rb new file mode 100644 index 00000000..61688aee --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/integration_test/templates/integration_test.rb @@ -0,0 +1,10 @@ +require "#{File.dirname(__FILE__)}<%= '/..' * class_nesting_depth %>/../test_helper" + +class <%= class_name %>Test < ActionController::IntegrationTest + # fixtures :your, :models + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/mailer/USAGE b/vendor/rails/railties/lib/rails_generator/generators/components/mailer/USAGE new file mode 100644 index 00000000..f3c295eb --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/mailer/USAGE @@ -0,0 +1,18 @@ +Description: + The mailer generator creates stubs for a new mailer and its views. + + The generator takes a mailer name and a list of views as arguments. + The mailer name may be given in CamelCase or under_score. + + The generator creates a mailer class in app/models with view templates + in app/views/mailer_name, and a test suite with fixtures in test/unit. + +Example: + ./script/generate mailer Notifications signup forgot_password invoice + + This will create a Notifications mailer class: + Mailer: app/models/notifications.rb + Views: app/views/notifications/signup.rhtml [...] + Test: test/unit/test/unit/notifications_test.rb + Fixtures: test/fixtures/notifications/signup [...] + diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/mailer/mailer_generator.rb b/vendor/rails/railties/lib/rails_generator/generators/components/mailer/mailer_generator.rb new file mode 100644 index 00000000..bac0cb01 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/mailer/mailer_generator.rb @@ -0,0 +1,32 @@ +class MailerGenerator < Rails::Generator::NamedBase + def manifest + record do |m| + # Check for class naming collisions. + m.class_collisions class_path, class_name, "#{class_name}Test" + + # Mailer, view, test, and fixture directories. + m.directory File.join('app/models', class_path) + m.directory File.join('app/views', class_path, file_name) + m.directory File.join('test/unit', class_path) + m.directory File.join('test/fixtures', class_path, file_name) + + # Mailer class and unit test. + m.template "mailer.rb", File.join('app/models', + class_path, + "#{file_name}.rb") + m.template "unit_test.rb", File.join('test/unit', + class_path, + "#{file_name}_test.rb") + + # View template and fixture for each action. + actions.each do |action| + m.template "view.rhtml", + File.join('app/views', class_path, file_name, "#{action}.rhtml"), + :assigns => { :action => action } + m.template "fixture.rhtml", + File.join('test/fixtures', class_path, file_name, action), + :assigns => { :action => action } + end + end + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/fixture.rhtml b/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/fixture.rhtml new file mode 100644 index 00000000..b4819068 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/fixture.rhtml @@ -0,0 +1,3 @@ +<%= class_name %>#<%= action %> + +Find me in app/views/<%= file_name %>/<%= action %>.rhtml diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/mailer.rb b/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/mailer.rb new file mode 100644 index 00000000..127495fc --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/mailer.rb @@ -0,0 +1,13 @@ +class <%= class_name %> < ActionMailer::Base +<% for action in actions -%> + + def <%= action %>(sent_at = Time.now) + @subject = '<%= class_name %>#<%= action %>' + @body = {} + @recipients = '' + @from = '' + @sent_on = sent_at + @headers = {} + end +<% end -%> +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/unit_test.rb b/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/unit_test.rb new file mode 100644 index 00000000..0512cad5 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/unit_test.rb @@ -0,0 +1,37 @@ +require File.dirname(__FILE__) + '/../test_helper' +require '<%= file_name %>' + +class <%= class_name %>Test < Test::Unit::TestCase + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures' + CHARSET = "utf-8" + + include ActionMailer::Quoting + + def setup + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries = [] + + @expected = TMail::Mail.new + @expected.set_content_type "text", "plain", { "charset" => CHARSET } + end + +<% for action in actions -%> + def test_<%= action %> + @expected.subject = '<%= class_name %>#<%= action %>' + @expected.body = read_fixture('<%= action %>') + @expected.date = Time.now + + assert_equal @expected.encoded, <%= class_name %>.create_<%= action %>(@expected.date).encoded + end + +<% end -%> + private + def read_fixture(action) + IO.readlines("#{FIXTURES_PATH}/<%= file_name %>/#{action}") + end + + def encode(subject) + quoted_printable(subject, CHARSET) + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/view.rhtml b/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/view.rhtml new file mode 100644 index 00000000..b4819068 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/view.rhtml @@ -0,0 +1,3 @@ +<%= class_name %>#<%= action %> + +Find me in app/views/<%= file_name %>/<%= action %>.rhtml diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/migration/USAGE b/vendor/rails/railties/lib/rails_generator/generators/components/migration/USAGE new file mode 100644 index 00000000..749076e9 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/migration/USAGE @@ -0,0 +1,14 @@ +Description: + The migration generator creates a stub for a new database migration. + + The generator takes a migration name as its argument. The migration name may be + given in CamelCase or under_score. + + The generator creates a migration class in db/migrate prefixed by its number + in the queue. + +Example: + ./script/generate migration AddSslFlag + + With 4 existing migrations, this will create an AddSslFlag migration in the + file db/migrate/005_add_ssl_flag.rb \ No newline at end of file diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/migration/migration_generator.rb b/vendor/rails/railties/lib/rails_generator/generators/components/migration/migration_generator.rb new file mode 100644 index 00000000..a0d0d472 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/migration/migration_generator.rb @@ -0,0 +1,7 @@ +class MigrationGenerator < Rails::Generator::NamedBase + def manifest + record do |m| + m.migration_template 'migration.rb', 'db/migrate' + end + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/migration/templates/migration.rb b/vendor/rails/railties/lib/rails_generator/generators/components/migration/templates/migration.rb new file mode 100644 index 00000000..9d0e5306 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/migration/templates/migration.rb @@ -0,0 +1,7 @@ +class <%= class_name %> < ActiveRecord::Migration + def self.up + end + + def self.down + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/model/USAGE b/vendor/rails/railties/lib/rails_generator/generators/components/model/USAGE new file mode 100644 index 00000000..57156ea5 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/model/USAGE @@ -0,0 +1,19 @@ +Description: + The model generator creates stubs for a new model. + + The generator takes a model name as its argument. The model name may be + given in CamelCase or under_score and should not be suffixed with 'Model'. + + The generator creates a model class in app/models, a test suite in + test/unit, test fixtures in test/fixtures/singular_name.yml, and a migration + in db/migrate. + +Example: + ./script/generate model Account + + This will create an Account model: + Model: app/models/account.rb + Test: test/unit/account_test.rb + Fixtures: test/fixtures/accounts.yml + Migration: db/migrate/XXX_add_accounts.rb + diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/model/model_generator.rb b/vendor/rails/railties/lib/rails_generator/generators/components/model/model_generator.rb new file mode 100644 index 00000000..e2424826 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/model/model_generator.rb @@ -0,0 +1,34 @@ +class ModelGenerator < Rails::Generator::NamedBase + default_options :skip_migration => false + + def manifest + record do |m| + # Check for class naming collisions. + m.class_collisions class_path, class_name, "#{class_name}Test" + + # Model, test, and fixture directories. + m.directory File.join('app/models', class_path) + m.directory File.join('test/unit', class_path) + m.directory File.join('test/fixtures', class_path) + + # Model class, unit test, and fixtures. + m.template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb") + m.template 'unit_test.rb', File.join('test/unit', class_path, "#{file_name}_test.rb") + m.template 'fixtures.yml', File.join('test/fixtures', class_path, "#{table_name}.yml") + + unless options[:skip_migration] + m.migration_template 'migration.rb', 'db/migrate', :assigns => { + :migration_name => "Create#{class_name.pluralize.gsub(/::/, '')}" + }, :migration_file_name => "create_#{file_path.gsub(/\//, '_').pluralize}" + end + end + end + + protected + def add_options!(opt) + opt.separator '' + opt.separator 'Options:' + opt.on("--skip-migration", + "Don't generate a migration file for this model") { |v| options[:skip_migration] = v } + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/model/templates/fixtures.yml b/vendor/rails/railties/lib/rails_generator/generators/components/model/templates/fixtures.yml new file mode 100644 index 00000000..8794d28a --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/model/templates/fixtures.yml @@ -0,0 +1,5 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +first: + id: 1 +another: + id: 2 diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/model/templates/migration.rb b/vendor/rails/railties/lib/rails_generator/generators/components/model/templates/migration.rb new file mode 100644 index 00000000..a6954ebd --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/model/templates/migration.rb @@ -0,0 +1,11 @@ +class <%= migration_name %> < ActiveRecord::Migration + def self.up + create_table :<%= table_name %> do |t| + # t.column :name, :string + end + end + + def self.down + drop_table :<%= table_name %> + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/model/templates/model.rb b/vendor/rails/railties/lib/rails_generator/generators/components/model/templates/model.rb new file mode 100644 index 00000000..8d4c89e9 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/model/templates/model.rb @@ -0,0 +1,2 @@ +class <%= class_name %> < ActiveRecord::Base +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/model/templates/unit_test.rb b/vendor/rails/railties/lib/rails_generator/generators/components/model/templates/unit_test.rb new file mode 100644 index 00000000..b464de47 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/model/templates/unit_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '<%= '/..' * class_nesting_depth %>/../test_helper' + +class <%= class_name %>Test < Test::Unit::TestCase + fixtures :<%= table_name %> + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/plugin/USAGE b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/USAGE new file mode 100644 index 00000000..f033c814 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/USAGE @@ -0,0 +1,35 @@ +Description: + The plugin generator creates stubs for a new plugin. + + The generator takes a plugin name as its argument. The plugin name may be + given in CamelCase or under_score and should not be suffixed with 'Plugin'. + + The generator creates a plugin directory in vendor/plugins that includes + both init.rb and README files as well as lib, task, and test directories. + + It's also possible to generate stub files for a generator to go with the + plugin by using --with-generator + +Example: + ./script/generate plugin BrowserFilters + + This will create: + vendor/plugins/browser_filters/README + vendor/plugins/browser_filters/init.rb + vendor/plugins/browser_filters/install.rb + vendor/plugins/browser_filters/lib/browser_filters.rb + vendor/plugins/browser_filters/test/browser_filters_test.rb + vendor/plugins/browser_filters/tasks/browser_filters_tasks.rake + + ./script/generate plugin BrowserFilters --with-generator + + This will create: + vendor/plugins/browser_filters/README + vendor/plugins/browser_filters/init.rb + vendor/plugins/browser_filters/install.rb + vendor/plugins/browser_filters/lib/browser_filters.rb + vendor/plugins/browser_filters/test/browser_filters_test.rb + vendor/plugins/browser_filters/tasks/browser_filters_tasks.rake + vendor/plugins/browser_filters/generators/browser_filters/browser_filters_generator.rb + vendor/plugins/browser_filters/generators/browser_filters/USAGE + vendor/plugins/browser_filters/generators/browser_filters/templates/ diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/plugin/plugin_generator.rb b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/plugin_generator.rb new file mode 100644 index 00000000..ea5fdf2b --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/plugin_generator.rb @@ -0,0 +1,34 @@ +class PluginGenerator < Rails::Generator::NamedBase + attr_reader :plugin_path + + def initialize(runtime_args, runtime_options = {}) + @with_generator = runtime_args.delete("--with-generator") + super + @plugin_path = "vendor/plugins/#{file_name}" + end + + def manifest + record do |m| + m.directory "#{plugin_path}/lib" + m.directory "#{plugin_path}/tasks" + m.directory "#{plugin_path}/test" + + m.template 'README', "#{plugin_path}/README" + m.template 'Rakefile', "#{plugin_path}/Rakefile" + m.template 'init.rb', "#{plugin_path}/init.rb" + m.template 'install.rb', "#{plugin_path}/install.rb" + m.template 'plugin.rb', "#{plugin_path}/lib/#{file_name}.rb" + m.template 'tasks.rake', "#{plugin_path}/tasks/#{file_name}_tasks.rake" + m.template 'unit_test.rb', "#{plugin_path}/test/#{file_name}_test.rb" + + if @with_generator + m.directory "#{plugin_path}/generators" + m.directory "#{plugin_path}/generators/#{file_name}" + m.directory "#{plugin_path}/generators/#{file_name}/templates" + + m.template 'generator.rb', "#{plugin_path}/generators/#{file_name}/#{file_name}_generator.rb" + m.template 'USAGE', "#{plugin_path}/generators/#{file_name}/USAGE" + end + end + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/README b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/README new file mode 100644 index 00000000..d7276413 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/README @@ -0,0 +1,4 @@ +<%= class_name %> +<%= "=" * class_name.size %> + +Description goes here \ No newline at end of file diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/Rakefile b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/Rakefile new file mode 100755 index 00000000..1824fb10 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the <%= file_name %> plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the <%= file_name %> plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = '<%= class_name %>' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/USAGE b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/USAGE new file mode 100644 index 00000000..f9277994 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/USAGE @@ -0,0 +1,8 @@ +Description: + Explain the generator + +Example: + ./script/generate <%= file_name %> Thing + + This will create: + what/will/it/create \ No newline at end of file diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/generator.rb b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/generator.rb new file mode 100644 index 00000000..3e800df6 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/generator.rb @@ -0,0 +1,8 @@ +class <%= class_name %>Generator < Rails::Generator::NamedBase + def manifest + record do |m| + # m.directory "lib" + # m.template 'README', "README" + end + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/init.rb b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/init.rb new file mode 100644 index 00000000..ada2eece --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/init.rb @@ -0,0 +1 @@ +# Include hook code here \ No newline at end of file diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/install.rb b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/install.rb new file mode 100644 index 00000000..f7732d37 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/install.rb @@ -0,0 +1 @@ +# Install hook code here diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/plugin.rb b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/plugin.rb new file mode 100644 index 00000000..1fa5b902 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/plugin.rb @@ -0,0 +1 @@ +# <%= class_name %> \ No newline at end of file diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/tasks.rake b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/tasks.rake new file mode 100644 index 00000000..5222b22c --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/tasks.rake @@ -0,0 +1,4 @@ +# desc "Explaining what the task does" +# task :<%= file_name %> do +# # Task goes here +# end \ No newline at end of file diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/unit_test.rb b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/unit_test.rb new file mode 100644 index 00000000..9028b84b --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/unit_test.rb @@ -0,0 +1,8 @@ +require 'test/unit' + +class <%= class_name %>Test < Test::Unit::TestCase + # Replace this with your real tests. + def test_this_plugin + flunk + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/USAGE b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/USAGE new file mode 100644 index 00000000..1b6eaa2d --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/USAGE @@ -0,0 +1,32 @@ +Description: + The scaffold generator creates a controller to interact with a model. + If the model does not exist, it creates the model as well. The generated + code is equivalent to the "scaffold :model" declaration, making it easy + to migrate when you wish to customize your controller and views. + + The generator takes a model name, an optional controller name, and a + list of views as arguments. Scaffolded actions and views are created + automatically. Any views left over generate empty stubs. + + The scaffolded actions and views are: + index, list, show, new, create, edit, update, destroy + + If a controller name is not given, the plural form of the model name + will be used. The model and controller names may be given in CamelCase + or under_score and should not be suffixed with 'Model' or 'Controller'. + Both model and controller names may be prefixed with a module like a + file path; see the Modules Example for usage. + +Example: + ./script/generate scaffold Account Bank debit credit + + This will generate an Account model and BankController with a full test + suite and a basic user interface. Now create the accounts table in your + database and browse to http://localhost/bank/ -- voila, you're on Rails! + +Modules Example: + ./script/generate scaffold CreditCard 'admin/credit_card' suspend late_fee + + This will generate a CreditCard model and CreditCardController controller + in the admin module. + diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/scaffold_generator.rb b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/scaffold_generator.rb new file mode 100644 index 00000000..d8e95382 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/scaffold_generator.rb @@ -0,0 +1,184 @@ +class ScaffoldingSandbox + include ActionView::Helpers::ActiveRecordHelper + + attr_accessor :form_action, :singular_name, :suffix, :model_instance + + def sandbox_binding + binding + end + + def default_input_block + Proc.new { |record, column| "


      \n#{input(record, column.name)}

      \n" } + end + +end + +class ActionView::Helpers::InstanceTag + def to_input_field_tag(field_type, options={}) + field_meth = "#{field_type}_field" + "<%= #{field_meth} '#{@object_name}', '#{@method_name}' #{options.empty? ? '' : ', '+options.inspect} %>" + end + + def to_text_area_tag(options = {}) + "<%= text_area '#{@object_name}', '#{@method_name}' #{options.empty? ? '' : ', '+ options.inspect} %>" + end + + def to_date_select_tag(options = {}) + "<%= date_select '#{@object_name}', '#{@method_name}' #{options.empty? ? '' : ', '+ options.inspect} %>" + end + + def to_datetime_select_tag(options = {}) + "<%= datetime_select '#{@object_name}', '#{@method_name}' #{options.empty? ? '' : ', '+ options.inspect} %>" + end +end + +class ScaffoldGenerator < Rails::Generator::NamedBase + attr_reader :controller_name, + :controller_class_path, + :controller_file_path, + :controller_class_nesting, + :controller_class_nesting_depth, + :controller_class_name, + :controller_singular_name, + :controller_plural_name + alias_method :controller_file_name, :controller_singular_name + alias_method :controller_table_name, :controller_plural_name + + def initialize(runtime_args, runtime_options = {}) + super + + # Take controller name from the next argument. Default to the pluralized model name. + @controller_name = args.shift + @controller_name ||= ActiveRecord::Base.pluralize_table_names ? @name.pluralize : @name + + base_name, @controller_class_path, @controller_file_path, @controller_class_nesting, @controller_class_nesting_depth = extract_modules(@controller_name) + @controller_class_name_without_nesting, @controller_singular_name, @controller_plural_name = inflect_names(base_name) + + if @controller_class_nesting.empty? + @controller_class_name = @controller_class_name_without_nesting + else + @controller_class_name = "#{@controller_class_nesting}::#{@controller_class_name_without_nesting}" + end + end + + def manifest + record do |m| + # Check for class naming collisions. + m.class_collisions controller_class_path, "#{controller_class_name}Controller", "#{controller_class_name}ControllerTest", "#{controller_class_name}Helper" + + # Controller, helper, views, and test directories. + m.directory File.join('app/controllers', controller_class_path) + m.directory File.join('app/helpers', controller_class_path) + m.directory File.join('app/views', controller_class_path, controller_file_name) + m.directory File.join('test/functional', controller_class_path) + + # Depend on model generator but skip if the model exists. + m.dependency 'model', [singular_name], :collision => :skip, :skip_migration => true + + # Scaffolded forms. + m.complex_template "form.rhtml", + File.join('app/views', + controller_class_path, + controller_file_name, + "_form.rhtml"), + :insert => 'form_scaffolding.rhtml', + :sandbox => lambda { create_sandbox }, + :begin_mark => 'form', + :end_mark => 'eoform', + :mark_id => singular_name + + + # Scaffolded views. + scaffold_views.each do |action| + m.template "view_#{action}.rhtml", + File.join('app/views', + controller_class_path, + controller_file_name, + "#{action}.rhtml"), + :assigns => { :action => action } + end + + # Controller class, functional test, helper, and views. + m.template 'controller.rb', + File.join('app/controllers', + controller_class_path, + "#{controller_file_name}_controller.rb") + + m.template 'functional_test.rb', + File.join('test/functional', + controller_class_path, + "#{controller_file_name}_controller_test.rb") + + m.template 'helper.rb', + File.join('app/helpers', + controller_class_path, + "#{controller_file_name}_helper.rb") + + # Layout and stylesheet. + m.template 'layout.rhtml', "app/views/layouts/#{controller_file_name}.rhtml" + m.template 'style.css', 'public/stylesheets/scaffold.css' + + + # Unscaffolded views. + unscaffolded_actions.each do |action| + path = File.join('app/views', + controller_class_path, + controller_file_name, + "#{action}.rhtml") + m.template "controller:view.rhtml", path, + :assigns => { :action => action, :path => path} + end + end + end + + protected + # Override with your own usage banner. + def banner + "Usage: #{$0} scaffold ModelName [ControllerName] [action, ...]" + end + + def scaffold_views + %w(list show new edit) + end + + def scaffold_actions + scaffold_views + %w(index create update destroy) + end + + def model_name + class_name.demodulize + end + + def unscaffolded_actions + args - scaffold_actions + end + + def suffix + "_#{singular_name}" if options[:suffix] + end + + def create_sandbox + sandbox = ScaffoldingSandbox.new + sandbox.singular_name = singular_name + begin + sandbox.model_instance = model_instance + sandbox.instance_variable_set("@#{singular_name}", sandbox.model_instance) + rescue ActiveRecord::StatementInvalid => e + logger.error "Before updating scaffolding from new DB schema, try creating a table for your model (#{class_name})" + raise SystemExit + end + sandbox.suffix = suffix + sandbox + end + + def model_instance + base = class_nesting.split('::').inject(Object) do |base, nested| + break base.const_get(nested) if base.const_defined?(nested) + base.const_set(nested, Module.new) + end + unless base.const_defined?(@class_name_without_nesting) + base.const_set(@class_name_without_nesting, Class.new(ActiveRecord::Base)) + end + class_name.constantize.new + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb new file mode 100644 index 00000000..c059e745 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb @@ -0,0 +1,58 @@ +class <%= controller_class_name %>Controller < ApplicationController +<% unless suffix -%> + def index + list + render :action => 'list' + end +<% end -%> + +<% for action in unscaffolded_actions -%> + def <%= action %><%= suffix %> + end + +<% end -%> + # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html) + verify :method => :post, :only => [ :destroy<%= suffix %>, :create<%= suffix %>, :update<%= suffix %> ], + :redirect_to => { :action => :list<%= suffix %> } + + def list<%= suffix %> + @<%= singular_name %>_pages, @<%= plural_name %> = paginate :<%= plural_name %>, :per_page => 10 + end + + def show<%= suffix %> + @<%= singular_name %> = <%= model_name %>.find(params[:id]) + end + + def new<%= suffix %> + @<%= singular_name %> = <%= model_name %>.new + end + + def create<%= suffix %> + @<%= singular_name %> = <%= model_name %>.new(params[:<%= singular_name %>]) + if @<%= singular_name %>.save + flash[:notice] = '<%= model_name %> was successfully created.' + redirect_to :action => 'list<%= suffix %>' + else + render :action => 'new<%= suffix %>' + end + end + + def edit<%= suffix %> + @<%= singular_name %> = <%= model_name %>.find(params[:id]) + end + + def update + @<%= singular_name %> = <%= model_name %>.find(params[:id]) + if @<%= singular_name %>.update_attributes(params[:<%= singular_name %>]) + flash[:notice] = '<%= model_name %> was successfully updated.' + redirect_to :action => 'show<%= suffix %>', :id => @<%= singular_name %> + else + render :action => 'edit<%= suffix %>' + end + end + + def destroy<%= suffix %> + <%= model_name %>.find(params[:id]).destroy + redirect_to :action => 'list<%= suffix %>' + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/form.rhtml b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/form.rhtml new file mode 100644 index 00000000..d15f0d4e --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/form.rhtml @@ -0,0 +1,3 @@ +<%%= error_messages_for '<%= singular_name %>' %> + +<%= template_for_inclusion %> diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/form_scaffolding.rhtml b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/form_scaffolding.rhtml new file mode 100644 index 00000000..c7a87553 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/form_scaffolding.rhtml @@ -0,0 +1 @@ +<%= all_input_tags(@model_instance, @singular_name, {}) %> \ No newline at end of file diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/functional_test.rb b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/functional_test.rb new file mode 100644 index 00000000..3441b566 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/functional_test.rb @@ -0,0 +1,98 @@ +require File.dirname(__FILE__) + '<%= "/.." * controller_class_nesting_depth %>/../test_helper' +require '<%= controller_file_path %>_controller' + +# Re-raise errors caught by the controller. +class <%= controller_class_name %>Controller; def rescue_action(e) raise e end; end + +class <%= controller_class_name %>ControllerTest < Test::Unit::TestCase + fixtures :<%= table_name %> + + def setup + @controller = <%= controller_class_name %>Controller.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + +<% for action in unscaffolded_actions -%> + def test_<%= action %> + get :<%= action %> + assert_response :success + assert_template '<%= action %>' + end + +<% end -%> +<% unless suffix -%> + def test_index + get :index + assert_response :success + assert_template 'list' + end + +<% end -%> + def test_list<%= suffix %> + get :list<%= suffix %> + + assert_response :success + assert_template 'list<%= suffix %>' + + assert_not_nil assigns(:<%= plural_name %>) + end + + def test_show<%= suffix %> + get :show<%= suffix %>, :id => 1 + + assert_response :success + assert_template 'show' + + assert_not_nil assigns(:<%= singular_name %>) + assert assigns(:<%= singular_name %>).valid? + end + + def test_new<%= suffix %> + get :new<%= suffix %> + + assert_response :success + assert_template 'new<%= suffix %>' + + assert_not_nil assigns(:<%= singular_name %>) + end + + def test_create + num_<%= plural_name %> = <%= model_name %>.count + + post :create<%= suffix %>, :<%= singular_name %> => {} + + assert_response :redirect + assert_redirected_to :action => 'list<%= suffix %>' + + assert_equal num_<%= plural_name %> + 1, <%= model_name %>.count + end + + def test_edit<%= suffix %> + get :edit<%= suffix %>, :id => 1 + + assert_response :success + assert_template 'edit<%= suffix %>' + + assert_not_nil assigns(:<%= singular_name %>) + assert assigns(:<%= singular_name %>).valid? + end + + def test_update<%= suffix %> + post :update<%= suffix %>, :id => 1 + assert_response :redirect + assert_redirected_to :action => 'show<%= suffix %>', :id => 1 + end + + def test_destroy<%= suffix %> + assert_not_nil <%= model_name %>.find(1) + + post :destroy, :id => 1 + assert_response :redirect + assert_redirected_to :action => 'list<%= suffix %>' + + assert_raise(ActiveRecord::RecordNotFound) { + <%= model_name %>.find(1) + } + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/helper.rb b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/helper.rb new file mode 100644 index 00000000..9bd821b1 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/helper.rb @@ -0,0 +1,2 @@ +module <%= controller_class_name %>Helper +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/layout.rhtml b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/layout.rhtml new file mode 100644 index 00000000..b5ba9a4e --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/layout.rhtml @@ -0,0 +1,13 @@ + + + <%= controller_class_name %>: <%%= controller.action_name %> + <%%= stylesheet_link_tag 'scaffold' %> + + + +

      <%%= flash[:notice] %>

      + +<%%= yield %> + + + diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/style.css b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/style.css new file mode 100644 index 00000000..8f239a35 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/style.css @@ -0,0 +1,74 @@ +body { background-color: #fff; color: #333; } + +body, p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +pre { + background-color: #eee; + padding: 10px; + font-size: 11px; +} + +a { color: #000; } +a:visited { color: #666; } +a:hover { color: #fff; background-color:#000; } + +.fieldWithErrors { + padding: 2px; + background-color: red; + display: table; +} + +#errorExplanation { + width: 400px; + border: 2px solid red; + padding: 7px; + padding-bottom: 12px; + margin-bottom: 20px; + background-color: #f0f0f0; +} + +#errorExplanation h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 12px; + margin: -7px; + background-color: #c00; + color: #fff; +} + +#errorExplanation p { + color: #333; + margin-bottom: 0; + padding: 5px; +} + +#errorExplanation ul li { + font-size: 12px; + list-style: square; +} + +div.uploadStatus { + margin: 5px; +} + +div.progressBar { + margin: 5px; +} + +div.progressBar div.border { + background-color: #fff; + border: 1px solid grey; + width: 100%; +} + +div.progressBar div.background { + background-color: #333; + height: 18px; + width: 0%; +} + diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_edit.rhtml b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_edit.rhtml new file mode 100644 index 00000000..2db0909c --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_edit.rhtml @@ -0,0 +1,9 @@ +

      Editing <%= singular_name %>

      + +<%%= start_form_tag :action => 'update<%= @suffix %>', :id => @<%= singular_name %> %> + <%%= render :partial => 'form' %> + <%%= submit_tag 'Edit' %> +<%%= end_form_tag %> + +<%%= link_to 'Show', :action => 'show<%= suffix %>', :id => @<%= singular_name %> %> | +<%%= link_to 'Back', :action => 'list<%= suffix %>' %> diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml new file mode 100644 index 00000000..ea1ca7ea --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml @@ -0,0 +1,27 @@ +

      Listing <%= plural_name %>

      + + + + <%% for column in <%= model_name %>.content_columns %> + + <%% end %> + + +<%% for <%= singular_name %> in @<%= plural_name %> %> + + <%% for column in <%= model_name %>.content_columns %> + + <%% end %> + + + + +<%% end %> +
      <%%= column.human_name %>
      <%%=h <%= singular_name %>.send(column.name) %><%%= link_to 'Show', :action => 'show<%= suffix %>', :id => <%= singular_name %> %><%%= link_to 'Edit', :action => 'edit<%= suffix %>', :id => <%= singular_name %> %><%%= link_to 'Destroy', { :action => 'destroy<%= suffix %>', :id => <%= singular_name %> }, :confirm => 'Are you sure?', :post => true %>
      + +<%%= link_to 'Previous page', { :page => @<%= singular_name %>_pages.current.previous } if @<%= singular_name %>_pages.current.previous %> +<%%= link_to 'Next page', { :page => @<%= singular_name %>_pages.current.next } if @<%= singular_name %>_pages.current.next %> + +
      + +<%%= link_to 'New <%= singular_name %>', :action => 'new<%= suffix %>' %> diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_new.rhtml b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_new.rhtml new file mode 100644 index 00000000..286f8507 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_new.rhtml @@ -0,0 +1,8 @@ +

      New <%= singular_name %>

      + +<%%= start_form_tag :action => 'create<%= @suffix %>' %> + <%%= render :partial => 'form' %> + <%%= submit_tag "Create" %> +<%%= end_form_tag %> + +<%%= link_to 'Back', :action => 'list<%= suffix %>' %> diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_show.rhtml b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_show.rhtml new file mode 100644 index 00000000..c9245cdf --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_show.rhtml @@ -0,0 +1,8 @@ +<%% for column in <%= model_name %>.content_columns %> +

      + <%%= column.human_name %>: <%%=h @<%= singular_name %>.send(column.name) %> +

      +<%% end %> + +<%%= link_to 'Edit', :action => 'edit<%= suffix %>', :id => @<%= singular_name %> %> | +<%%= link_to 'Back', :action => 'list<%= suffix %>' %> diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/session_migration/USAGE b/vendor/rails/railties/lib/rails_generator/generators/components/session_migration/USAGE new file mode 100644 index 00000000..3234a4ef --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/session_migration/USAGE @@ -0,0 +1,15 @@ +Description: + The session table migration generator creates a migration for adding a session table + used by CGI::Session::ActiveRecordStore. + + The generator takes a migration name as its argument. The migration name may be + given in CamelCase or under_score. + + The generator creates a migration class in db/migrate prefixed by its number + in the queue. + +Example: + ./script/generate session_migration AddSessionTable + + With 4 existing migrations, this will create an AddSessionTable migration in the + file db/migrate/005_add_session_table.rb \ No newline at end of file diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/session_migration/session_migration_generator.rb b/vendor/rails/railties/lib/rails_generator/generators/components/session_migration/session_migration_generator.rb new file mode 100644 index 00000000..a513fa5c --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/session_migration/session_migration_generator.rb @@ -0,0 +1,12 @@ +class SessionMigrationGenerator < Rails::Generator::NamedBase + def initialize(runtime_args, runtime_options = {}) + runtime_args << 'add_session_table' if runtime_args.empty? + super + end + + def manifest + record do |m| + m.migration_template 'migration.rb', 'db/migrate' + end + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/session_migration/templates/migration.rb b/vendor/rails/railties/lib/rails_generator/generators/components/session_migration/templates/migration.rb new file mode 100644 index 00000000..0ab7fcb1 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/session_migration/templates/migration.rb @@ -0,0 +1,15 @@ +class <%= class_name %> < ActiveRecord::Migration + def self.up + create_table :sessions do |t| + t.column :session_id, :string + t.column :data, :text + t.column :updated_at, :datetime + end + + add_index :sessions, :session_id + end + + def self.down + drop_table :sessions + end +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/web_service/USAGE b/vendor/rails/railties/lib/rails_generator/generators/components/web_service/USAGE new file mode 100644 index 00000000..d3e45b7f --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/web_service/USAGE @@ -0,0 +1,28 @@ +Description: + The web service generator creates the controller and API definition for + a web service. + + The generator takes a web service name and a list of API methods as arguments. + The web service name may be given in CamelCase or under_score and should + contain no extra suffixes. To create a web service within a + module, specify the web service name as 'module/webservice'. + + The generator creates a controller class in app/controllers, an API definition + in app/apis, and a functional test suite in test/functional. + +Example: + ./script/generate web_service User add edit list remove + + User web service. + Controller: app/controllers/user_controller.rb + API: app/apis/user_api.rb + Test: test/functional/user_api_test.rb + +Modules Example: + ./script/generate web_service 'api/registration' register renew + + Registration web service. + Controller: app/controllers/api/registration_controller.rb + API: app/apis/api/registration_api.rb + Test: test/functional/api/registration_api_test.rb + diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/web_service/templates/api_definition.rb b/vendor/rails/railties/lib/rails_generator/generators/components/web_service/templates/api_definition.rb new file mode 100644 index 00000000..97d0b608 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/web_service/templates/api_definition.rb @@ -0,0 +1,5 @@ +class <%= class_name %>Api < ActionWebService::API::Base +<% for method_name in args -%> + api_method :<%= method_name %> +<% end -%> +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/web_service/templates/controller.rb b/vendor/rails/railties/lib/rails_generator/generators/components/web_service/templates/controller.rb new file mode 100644 index 00000000..7b0a8657 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/web_service/templates/controller.rb @@ -0,0 +1,8 @@ +class <%= class_name %>Controller < ApplicationController + wsdl_service_name '<%= class_name %>' +<% for method_name in args -%> + + def <%= method_name %> + end +<% end -%> +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/web_service/templates/functional_test.rb b/vendor/rails/railties/lib/rails_generator/generators/components/web_service/templates/functional_test.rb new file mode 100644 index 00000000..c4d136f8 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/web_service/templates/functional_test.rb @@ -0,0 +1,19 @@ +require File.dirname(__FILE__) + '<%= '/..' * class_nesting_depth %>/../test_helper' +require '<%= file_path %>_controller' + +class <%= class_name %>Controller; def rescue_action(e) raise e end; end + +class <%= class_name %>ControllerApiTest < Test::Unit::TestCase + def setup + @controller = <%= class_name %>Controller.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end +<% for method_name in args -%> + + def test_<%= method_name %> + result = invoke :<%= method_name %> + assert_equal nil, result + end +<% end -%> +end diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/web_service/web_service_generator.rb b/vendor/rails/railties/lib/rails_generator/generators/components/web_service/web_service_generator.rb new file mode 100644 index 00000000..ee18bf80 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/generators/components/web_service/web_service_generator.rb @@ -0,0 +1,29 @@ +class WebServiceGenerator < Rails::Generator::NamedBase + def manifest + record do |m| + # Check for class naming collisions. + m.class_collisions class_path, "#{class_name}Api", "#{class_name}Controller", "#{class_name}ApiTest" + + # API and test directories. + m.directory File.join('app/apis', class_path) + m.directory File.join('app/controllers', class_path) + m.directory File.join('test/functional', class_path) + + # API definition, controller, and functional test. + m.template 'api_definition.rb', + File.join('app/apis', + class_path, + "#{file_name}_api.rb") + + m.template 'controller.rb', + File.join('app/controllers', + class_path, + "#{file_name}_controller.rb") + + m.template 'functional_test.rb', + File.join('test/functional', + class_path, + "#{file_name}_api_test.rb") + end + end +end diff --git a/vendor/rails/railties/lib/rails_generator/lookup.rb b/vendor/rails/railties/lib/rails_generator/lookup.rb new file mode 100644 index 00000000..6ec68864 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/lookup.rb @@ -0,0 +1,210 @@ +require File.dirname(__FILE__) + '/spec' + +class Object + class << self + # Lookup missing generators using const_missing. This allows any + # generator to reference another without having to know its location: + # RubyGems, ~/.rails/generators, and RAILS_ROOT/generators. + def lookup_missing_generator(class_id) + if md = /(.+)Generator$/.match(class_id.to_s) + name = md.captures.first.demodulize.underscore + Rails::Generator::Base.lookup(name).klass + else + const_missing_before_generators(class_id) + end + end + + unless respond_to?(:const_missing_before_generators) + alias_method :const_missing_before_generators, :const_missing + alias_method :const_missing, :lookup_missing_generator + end + end +end + +# User home directory lookup adapted from RubyGems. +def Dir.user_home + if ENV['HOME'] + ENV['HOME'] + elsif ENV['USERPROFILE'] + ENV['USERPROFILE'] + elsif ENV['HOMEDRIVE'] and ENV['HOMEPATH'] + "#{ENV['HOMEDRIVE']}:#{ENV['HOMEPATH']}" + else + File.expand_path '~' + end +end + + +module Rails + module Generator + + # Generator lookup is managed by a list of sources which return specs + # describing where to find and how to create generators. This module + # provides class methods for manipulating the source list and looking up + # generator specs, and an #instance wrapper for quickly instantiating + # generators by name. + # + # A spec is not a generator: it's a description of where to find + # the generator and how to create it. A source is anything that + # yields generators from #each. PathSource and GemSource are provided. + module Lookup + def self.append_features(base) + super + base.extend(ClassMethods) + base.use_component_sources! + end + + # Convenience method to instantiate another generator. + def instance(generator_name, args, runtime_options = {}) + self.class.instance(generator_name, args, runtime_options) + end + + module ClassMethods + # The list of sources where we look, in order, for generators. + def sources + read_inheritable_attribute(:sources) or use_component_sources! + end + + # Add a source to the end of the list. + def append_sources(*args) + sources.concat(args.flatten) + invalidate_cache! + end + + # Add a source to the beginning of the list. + def prepend_sources(*args) + write_inheritable_array(:sources, args.flatten + sources) + invalidate_cache! + end + + # Reset the source list. + def reset_sources + write_inheritable_attribute(:sources, []) + invalidate_cache! + end + + # Use application generators (app, ?). + def use_application_sources! + reset_sources + sources << PathSource.new(:builtin, "#{File.dirname(__FILE__)}/generators/applications") + end + + # Use component generators (model, controller, etc). + # 1. Rails application. If RAILS_ROOT is defined we know we're + # generating in the context of a Rails application, so search + # RAILS_ROOT/generators. + # 2. User home directory. Search ~/.rails/generators. + # 3. RubyGems. Search for gems named *_generator. + # 4. Builtins. Model, controller, mailer, scaffold. + def use_component_sources! + reset_sources + if defined? ::RAILS_ROOT + sources << PathSource.new(:lib, "#{::RAILS_ROOT}/lib/generators") + sources << PathSource.new(:vendor, "#{::RAILS_ROOT}/vendor/generators") + sources << PathSource.new(:plugins, "#{::RAILS_ROOT}/vendor/plugins/**/generators") + end + sources << PathSource.new(:user, "#{Dir.user_home}/.rails/generators") + sources << GemSource.new if Object.const_defined?(:Gem) + sources << PathSource.new(:builtin, "#{File.dirname(__FILE__)}/generators/components") + end + + # Lookup knows how to find generators' Specs from a list of Sources. + # Searches the sources, in order, for the first matching name. + def lookup(generator_name) + @found ||= {} + generator_name = generator_name.to_s.downcase + @found[generator_name] ||= cache.find { |spec| spec.name == generator_name } + unless @found[generator_name] + chars = generator_name.scan(/./).map{|c|"#{c}.*?"} + rx = /^#{chars}$/ + gns = cache.select{|spec| spec.name =~ rx } + @found[generator_name] ||= gns.first if gns.length == 1 + raise GeneratorError, "Pattern '#{generator_name}' matches more than one generator: #{gns.map{|sp|sp.name}.join(', ')}" if gns.length > 1 + end + @found[generator_name] or raise GeneratorError, "Couldn't find '#{generator_name}' generator" + end + + # Convenience method to lookup and instantiate a generator. + def instance(generator_name, args = [], runtime_options = {}) + lookup(generator_name).klass.new(args, full_options(runtime_options)) + end + + private + # Lookup and cache every generator from the source list. + def cache + @cache ||= sources.inject([]) { |cache, source| cache + source.map } + end + + # Clear the cache whenever the source list changes. + def invalidate_cache! + @cache = nil + end + end + end + + # Sources enumerate (yield from #each) generator specs which describe + # where to find and how to create generators. Enumerable is mixed in so, + # for example, source.collect will retrieve every generator. + # Sources may be assigned a label to distinguish them. + class Source + include Enumerable + + attr_reader :label + def initialize(label) + @label = label + end + + # The each method must be implemented in subclasses. + # The base implementation raises an error. + def each + raise NotImplementedError + end + + # Return a convenient sorted list of all generator names. + def names + map { |spec| spec.name }.sort + end + end + + + # PathSource looks for generators in a filesystem directory. + class PathSource < Source + attr_reader :path + + def initialize(label, path) + super label + @path = path + end + + # Yield each eligible subdirectory. + def each + Dir["#{path}/[a-z]*"].each do |dir| + if File.directory?(dir) + yield Spec.new(File.basename(dir), dir, label) + end + end + end + end + + + # GemSource hits the mines to quarry for generators. The latest versions + # of gems named *_generator are selected. + class GemSource < Source + def initialize + super :RubyGems + end + + # Yield latest versions of generator gems. + def each + Gem::cache.search(/_generator$/).inject({}) { |latest, gem| + hem = latest[gem.name] + latest[gem.name] = gem if hem.nil? or gem.version > hem.version + latest + }.values.each { |gem| + yield Spec.new(gem.name.sub(/_generator$/, ''), gem.full_gem_path, label) + } + end + end + + end +end diff --git a/vendor/rails/railties/lib/rails_generator/manifest.rb b/vendor/rails/railties/lib/rails_generator/manifest.rb new file mode 100644 index 00000000..702effa7 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/manifest.rb @@ -0,0 +1,53 @@ +module Rails + module Generator + + # Manifest captures the actions a generator performs. Instantiate + # a manifest with an optional target object, hammer it with actions, + # then replay or rewind on the object of your choice. + # + # Example: + # manifest = Manifest.new { |m| + # m.make_directory '/foo' + # m.create_file '/foo/bar.txt' + # } + # manifest.replay(creator) + # manifest.rewind(destroyer) + class Manifest + attr_reader :target + + # Take a default action target. Yield self if block given. + def initialize(target = nil) + @target, @actions = target, [] + yield self if block_given? + end + + # Record an action. + def method_missing(action, *args, &block) + @actions << [action, args, block] + end + + # Replay recorded actions. + def replay(target = nil) + send_actions(target || @target, @actions) + end + + # Rewind recorded actions. + def rewind(target = nil) + send_actions(target || @target, @actions.reverse) + end + + # Erase recorded actions. + def erase + @actions = [] + end + + private + def send_actions(target, actions) + actions.each do |method, args, block| + target.send(method, *args, &block) + end + end + end + + end +end diff --git a/vendor/rails/railties/lib/rails_generator/options.rb b/vendor/rails/railties/lib/rails_generator/options.rb new file mode 100644 index 00000000..5cafa557 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/options.rb @@ -0,0 +1,140 @@ +require 'optparse' + +module Rails + module Generator + module Options + def self.append_features(base) + super + base.extend(ClassMethods) + class << base + if respond_to?(:inherited) + alias_method :inherited_without_options, :inherited + end + alias_method :inherited, :inherited_with_options + end + end + + module ClassMethods + def inherited_with_options(sub) + inherited_without_options(sub) if respond_to?(:inherited_without_options) + sub.extend(Rails::Generator::Options::ClassMethods) + end + + def mandatory_options(options = nil) + if options + write_inheritable_attribute(:mandatory_options, options) + else + read_inheritable_attribute(:mandatory_options) or write_inheritable_attribute(:mandatory_options, {}) + end + end + + def default_options(options = nil) + if options + write_inheritable_attribute(:default_options, options) + else + read_inheritable_attribute(:default_options) or write_inheritable_attribute(:default_options, {}) + end + end + + # Merge together our class options. In increasing precedence: + # default_options (class default options) + # runtime_options (provided as argument) + # mandatory_options (class mandatory options) + def full_options(runtime_options = {}) + default_options.merge(runtime_options).merge(mandatory_options) + end + + end + + # Each instance has an options hash that's populated by #parse. + def options + @options ||= {} + end + attr_writer :options + + protected + # Convenient access to class mandatory options. + def mandatory_options + self.class.mandatory_options + end + + # Convenient access to class default options. + def default_options + self.class.default_options + end + + # Merge together our instance options. In increasing precedence: + # default_options (class default options) + # options (instance options) + # runtime_options (provided as argument) + # mandatory_options (class mandatory options) + def full_options(runtime_options = {}) + self.class.full_options(options.merge(runtime_options)) + end + + # Parse arguments into the options hash. Classes may customize + # parsing behavior by overriding these methods: + # #banner Usage: ./script/generate [options] + # #add_options! Options: + # some options.. + # #add_general_options! General Options: + # general options.. + def parse!(args, runtime_options = {}) + self.options = {} + + @option_parser = OptionParser.new do |opt| + opt.banner = banner + add_options!(opt) + add_general_options!(opt) + opt.parse!(args) + end + + return args + ensure + self.options = full_options(runtime_options) + end + + # Raise a usage error. Override usage_message to provide a blurb + # after the option parser summary. + def usage(message = usage_message) + raise UsageError, "#{@option_parser}\n#{message}" + end + + def usage_message + '' + end + + # Override with your own usage banner. + def banner + "Usage: #{$0} [options]" + end + + # Override to add your options to the parser: + # def add_options!(opt) + # opt.on('-v', '--verbose') { |value| options[:verbose] = value } + # end + def add_options!(opt) + end + + # Adds general options like -h and --quiet. Usually don't override. + def add_general_options!(opt) + opt.separator '' + opt.separator 'General Options:' + + opt.on('-p', '--pretend', 'Run but do not make any changes.') { |v| options[:pretend] = v } + opt.on('-f', '--force', 'Overwrite files that already exist.') { options[:collision] = :force } + opt.on('-s', '--skip', 'Skip files that already exist.') { options[:collision] = :skip } + opt.on('-q', '--quiet', 'Suppress normal output.') { |v| options[:quiet] = v } + opt.on('-t', '--backtrace', 'Debugging: show backtrace on errors.') { |v| options[:backtrace] = v } + opt.on('-h', '--help', 'Show this help message.') { |v| options[:help] = v } + opt.on('-c', '--svn', 'Modify files with subversion. (Note: svn must be in path)') do + options[:svn] = `svn status`.inject({}) do |opt, e| + opt[e.chomp[7..-1]] = true + opt + end + end + end + + end + end +end diff --git a/vendor/rails/railties/lib/rails_generator/scripts.rb b/vendor/rails/railties/lib/rails_generator/scripts.rb new file mode 100644 index 00000000..14156e9c --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/scripts.rb @@ -0,0 +1,83 @@ +require File.dirname(__FILE__) + '/options' + +module Rails + module Generator + module Scripts + + # Generator scripts handle command-line invocation. Each script + # responds to an invoke! class method which handles option parsing + # and generator invocation. + class Base + include Options + default_options :collision => :ask, :quiet => false + + # Run the generator script. Takes an array of unparsed arguments + # and a hash of parsed arguments, takes the generator as an option + # or first remaining argument, and invokes the requested command. + def run(args = [], runtime_options = {}) + begin + parse!(args.dup, runtime_options) + rescue OptionParser::InvalidOption => e + # Don't cry, script. Generators want what you think is invalid. + end + + # Generator name is the only required option. + unless options[:generator] + usage if args.empty? + options[:generator] ||= args.shift + end + + # Look up generator instance and invoke command on it. + Rails::Generator::Base.instance(options[:generator], args, options).command(options[:command]).invoke! + rescue => e + puts e + puts " #{e.backtrace.join("\n ")}\n" if options[:backtrace] + raise SystemExit + end + + protected + # Override with your own script usage banner. + def banner + "Usage: #{$0} generator [options] [args]" + end + + def usage_message + usage = "\nInstalled Generators\n" + Rails::Generator::Base.sources.each do |source| + label = source.label.to_s.capitalize + names = source.names + usage << " #{label}: #{names.join(', ')}\n" unless names.empty? + end + + usage << < :destroy + end +end diff --git a/vendor/rails/railties/lib/rails_generator/scripts/generate.rb b/vendor/rails/railties/lib/rails_generator/scripts/generate.rb new file mode 100644 index 00000000..1fe2f54a --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/scripts/generate.rb @@ -0,0 +1,7 @@ +require File.dirname(__FILE__) + '/../scripts' + +module Rails::Generator::Scripts + class Generate < Base + mandatory_options :command => :create + end +end diff --git a/vendor/rails/railties/lib/rails_generator/scripts/update.rb b/vendor/rails/railties/lib/rails_generator/scripts/update.rb new file mode 100644 index 00000000..53a9faa3 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/scripts/update.rb @@ -0,0 +1,12 @@ +require File.dirname(__FILE__) + '/../scripts' + +module Rails::Generator::Scripts + class Update < Base + mandatory_options :command => :update + + protected + def banner + "Usage: #{$0} [options] scaffold" + end + end +end diff --git a/vendor/rails/railties/lib/rails_generator/simple_logger.rb b/vendor/rails/railties/lib/rails_generator/simple_logger.rb new file mode 100644 index 00000000..d750f07b --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/simple_logger.rb @@ -0,0 +1,46 @@ +module Rails + module Generator + class SimpleLogger # :nodoc: + attr_reader :out + attr_accessor :quiet + + def initialize(out = $stdout) + @out = out + @quiet = false + @level = 0 + end + + def log(status, message, &block) + @out.print("%12s %s%s\n" % [status, ' ' * @level, message]) unless quiet + indent(&block) if block_given? + end + + def indent(&block) + @level += 1 + if block_given? + begin + block.call + ensure + outdent + end + end + end + + def outdent + @level -= 1 + if block_given? + begin + block.call + ensure + indent + end + end + end + + private + def method_missing(method, *args, &block) + log(method.to_s, args.first, &block) + end + end + end +end diff --git a/vendor/rails/railties/lib/rails_generator/spec.rb b/vendor/rails/railties/lib/rails_generator/spec.rb new file mode 100644 index 00000000..ad609b8f --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/spec.rb @@ -0,0 +1,44 @@ +module Rails + module Generator + # A spec knows where a generator was found and how to instantiate it. + # Metadata include the generator's name, its base path, and the source + # which yielded it (PathSource, GemSource, etc.) + class Spec + attr_reader :name, :path, :source + + def initialize(name, path, source) + @name, @path, @source = name, path, source + end + + # Look up the generator class. Require its class file, find the class + # in ObjectSpace, tag it with this spec, and return. + def klass + unless @klass + require class_file + @klass = lookup_class + @klass.spec = self + end + @klass + end + + def class_file + "#{path}/#{name}_generator.rb" + end + + def class_name + "#{name.camelize}Generator" + end + + private + # Search for the first Class descending from Rails::Generator::Base + # whose name matches the requested class name. + def lookup_class + ObjectSpace.each_object(Class) do |obj| + return obj if obj.ancestors.include?(Rails::Generator::Base) and + obj.name.split('::').last == class_name + end + raise NameError, "Missing #{class_name} class in #{class_file}" + end + end + end +end diff --git a/vendor/rails/railties/lib/railties_path.rb b/vendor/rails/railties/lib/railties_path.rb new file mode 100644 index 00000000..81794050 --- /dev/null +++ b/vendor/rails/railties/lib/railties_path.rb @@ -0,0 +1 @@ +RAILTIES_PATH = File.expand_path(File.join(File.dirname(__FILE__), '..')) \ No newline at end of file diff --git a/vendor/rails/railties/lib/ruby_version_check.rb b/vendor/rails/railties/lib/ruby_version_check.rb new file mode 100644 index 00000000..68d3acc8 --- /dev/null +++ b/vendor/rails/railties/lib/ruby_version_check.rb @@ -0,0 +1,17 @@ +min_release = "1.8.2 (2004-12-25)" +ruby_release = "#{RUBY_VERSION} (#{RUBY_RELEASE_DATE})" +if ruby_release =~ /1\.8\.3/ + abort <<-end_message + + Rails does not work with Ruby version 1.8.3. + Please upgrade to version 1.8.4 or downgrade to 1.8.2. + + end_message +elsif ruby_release < min_release + abort <<-end_message + + Rails requires Ruby version #{min_release} or later. + You're running #{ruby_release}; please upgrade to continue. + + end_message +end diff --git a/vendor/rails/railties/lib/rubyprof_ext.rb b/vendor/rails/railties/lib/rubyprof_ext.rb new file mode 100644 index 00000000..f6e90357 --- /dev/null +++ b/vendor/rails/railties/lib/rubyprof_ext.rb @@ -0,0 +1,35 @@ +require 'prof' + +module Prof #:nodoc: + # Adapted from Shugo Maeda's unprof.rb + def self.print_profile(results, io = $stderr) + total = results.detect { |i| + i.method_class.nil? && i.method_id == :"#toplevel" + }.total_time + total = 0.001 if total < 0.001 + + io.puts " %% cumulative self self total" + io.puts " time seconds seconds calls ms/call ms/call name" + + sum = 0.0 + for r in results + sum += r.self_time + + name = if r.method_class.nil? + r.method_id.to_s + elsif r.method_class.is_a?(Class) + "#{r.method_class}##{r.method_id}" + else + "#{r.method_class}.#{r.method_id}" + end + io.printf "%6.2f %8.3f %8.3f %8d %8.2f %8.2f %s\n", + r.self_time / total * 100, + sum, + r.self_time, + r.count, + r.self_time * 1000 / r.count, + r.total_time * 1000 / r.count, + name + end + end +end diff --git a/vendor/rails/railties/lib/tasks/databases.rake b/vendor/rails/railties/lib/tasks/databases.rake new file mode 100644 index 00000000..e3272b86 --- /dev/null +++ b/vendor/rails/railties/lib/tasks/databases.rake @@ -0,0 +1,161 @@ +namespace :db do + desc "Migrate the database through scripts in db/migrate. Target specific version with VERSION=x" + task :migrate => :environment do + ActiveRecord::Migrator.migrate("db/migrate/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil) + Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + end + + namespace :fixtures do + desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y" + task :load => :environment do + require 'active_record/fixtures' + ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym) + (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir.glob(File.join(RAILS_ROOT, 'test', 'fixtures', '*.{yml,csv}'))).each do |fixture_file| + Fixtures.create_fixtures('test/fixtures', File.basename(fixture_file, '.*')) + end + end + end + + namespace :schema do + desc "Create a db/schema.rb file that can be portably used against any DB supported by AR" + task :dump => :environment do + require 'active_record/schema_dumper' + File.open(ENV['SCHEMA'] || "db/schema.rb", "w") do |file| + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) + end + end + + desc "Load a schema.rb file into the database" + task :load => :environment do + file = ENV['SCHEMA'] || "db/schema.rb" + load(file) + end + end + + namespace :structure do + desc "Dump the database structure to a SQL file" + task :dump => :environment do + abcs = ActiveRecord::Base.configurations + case abcs[RAILS_ENV]["adapter"] + when "mysql", "oci" + ActiveRecord::Base.establish_connection(abcs[RAILS_ENV]) + File.open("db/#{RAILS_ENV}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump } + when "postgresql" + ENV['PGHOST'] = abcs[RAILS_ENV]["host"] if abcs[RAILS_ENV]["host"] + ENV['PGPORT'] = abcs[RAILS_ENV]["port"].to_s if abcs[RAILS_ENV]["port"] + ENV['PGPASSWORD'] = abcs[RAILS_ENV]["password"].to_s if abcs[RAILS_ENV]["password"] + search_path = abcs[RAILS_ENV]["schema_search_path"] + search_path = "--schema=#{search_path}" if search_path + `pg_dump -i -U "#{abcs[RAILS_ENV]["username"]}" -s -x -O -f db/#{RAILS_ENV}_structure.sql #{search_path} #{abcs[RAILS_ENV]["database"]}` + raise "Error dumping database" if $?.exitstatus == 1 + when "sqlite", "sqlite3" + dbfile = abcs[RAILS_ENV]["database"] || abcs[RAILS_ENV]["dbfile"] + `#{abcs[RAILS_ENV]["adapter"]} #{dbfile} .schema > db/#{RAILS_ENV}_structure.sql` + when "sqlserver" + `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /f db\\#{RAILS_ENV}_structure.sql /q /A /r` + `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /F db\ /q /A /r` + else + raise "Task not supported by '#{abcs["test"]["adapter"]}'" + end + + if ActiveRecord::Base.connection.supports_migrations? + File.open("db/#{RAILS_ENV}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } + end + end + end + + namespace :test do + desc "Recreate the test database from the current environment's database schema" + task :clone => "db:schema:dump" do + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) + ActiveRecord::Schema.verbose = false + Rake::Task["db:schema:load"].invoke + end + + + desc "Recreate the test databases from the development structure" + task :clone_structure => [ "db:structure:dump", "db:test:purge" ] do + abcs = ActiveRecord::Base.configurations + case abcs["test"]["adapter"] + when "mysql" + ActiveRecord::Base.establish_connection(:test) + ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0') + IO.readlines("db/#{RAILS_ENV}_structure.sql").join.split("\n\n").each do |table| + ActiveRecord::Base.connection.execute(table) + end + when "postgresql" + ENV['PGHOST'] = abcs["test"]["host"] if abcs["test"]["host"] + ENV['PGPORT'] = abcs["test"]["port"].to_s if abcs["test"]["port"] + ENV['PGPASSWORD'] = abcs["test"]["password"].to_s if abcs["test"]["password"] + `psql -U "#{abcs["test"]["username"]}" -f db/#{RAILS_ENV}_structure.sql #{abcs["test"]["database"]}` + when "sqlite", "sqlite3" + dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"] + `#{abcs["test"]["adapter"]} #{dbfile} < db/#{RAILS_ENV}_structure.sql` + when "sqlserver" + `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql` + when "oci" + ActiveRecord::Base.establish_connection(:test) + IO.readlines("db/#{RAILS_ENV}_structure.sql").join.split(";\n\n").each do |ddl| + ActiveRecord::Base.connection.execute(ddl) + end + else + raise "Task not supported by '#{abcs["test"]["adapter"]}'" + end + end + + desc "Empty the test database" + task :purge => :environment do + abcs = ActiveRecord::Base.configurations + case abcs["test"]["adapter"] + when "mysql" + ActiveRecord::Base.establish_connection(:test) + ActiveRecord::Base.connection.recreate_database(abcs["test"]["database"]) + when "postgresql" + ENV['PGHOST'] = abcs["test"]["host"] if abcs["test"]["host"] + ENV['PGPORT'] = abcs["test"]["port"].to_s if abcs["test"]["port"] + ENV['PGPASSWORD'] = abcs["test"]["password"].to_s if abcs["test"]["password"] + enc_option = "-E #{abcs["test"]["encoding"]}" if abcs["test"]["encoding"] + `dropdb -U "#{abcs["test"]["username"]}" #{abcs["test"]["database"]}` + `createdb #{enc_option} -U "#{abcs["test"]["username"]}" #{abcs["test"]["database"]}` + when "sqlite","sqlite3" + dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"] + File.delete(dbfile) if File.exist?(dbfile) + when "sqlserver" + dropfkscript = "#{abcs["test"]["host"]}.#{abcs["test"]["database"]}.DP1".gsub(/\\/,'-') + `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{dropfkscript}` + `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql` + when "oci" + ActiveRecord::Base.establish_connection(:test) + ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl| + ActiveRecord::Base.connection.execute(ddl) + end + else + raise "Task not supported by '#{abcs["test"]["adapter"]}'" + end + end + + desc 'Prepare the test database and load the schema' + task :prepare => :environment do + Rake::Task[{ :sql => "db:test:clone_structure", :ruby => "db:test:clone" }[ActiveRecord::Base.schema_format]].invoke + end + end + + namespace :sessions do + desc "Creates a sessions table for use with CGI::Session::ActiveRecordStore" + task :create => :environment do + raise "Task unavailable to this database (no migration support)" unless ActiveRecord::Base.connection.supports_migrations? + require 'rails_generator' + require 'rails_generator/scripts/generate' + Rails::Generator::Scripts::Generate.new.run(["session_migration", ENV["MIGRATION"] || "AddSessions"]) + end + + desc "Clear the sessions table" + task :clear => :environment do + ActiveRecord::Base.connection.execute "DELETE FROM sessions" + end + end +end + +def session_table_name + ActiveRecord::Base.pluralize_table_names ? :sessions : :session +end diff --git a/vendor/rails/railties/lib/tasks/documentation.rake b/vendor/rails/railties/lib/tasks/documentation.rake new file mode 100644 index 00000000..2e5e7731 --- /dev/null +++ b/vendor/rails/railties/lib/tasks/documentation.rake @@ -0,0 +1,81 @@ +namespace :doc do + desc "Generate documentation for the application" + Rake::RDocTask.new("app") { |rdoc| + rdoc.rdoc_dir = 'doc/app' + rdoc.title = "Rails Application Documentation" + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('doc/README_FOR_APP') + rdoc.rdoc_files.include('app/**/*.rb') + } + + desc "Generate documentation for the Rails framework" + Rake::RDocTask.new("rails") { |rdoc| + rdoc.rdoc_dir = 'doc/api' + rdoc.template = "#{ENV['template']}.rb" if ENV['template'] + rdoc.title = "Rails Framework Documentation" + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('vendor/rails/railties/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/railties/MIT-LICENSE') + rdoc.rdoc_files.include('vendor/rails/activerecord/README') + rdoc.rdoc_files.include('vendor/rails/activerecord/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/activerecord/lib/active_record/**/*.rb') + rdoc.rdoc_files.exclude('vendor/rails/activerecord/lib/active_record/vendor/*') + rdoc.rdoc_files.include('vendor/rails/actionpack/README') + rdoc.rdoc_files.include('vendor/rails/actionpack/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/actionpack/lib/action_controller/**/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionpack/lib/action_view/**/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionmailer/README') + rdoc.rdoc_files.include('vendor/rails/actionmailer/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/actionmailer/lib/action_mailer/base.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/README') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/api/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/client/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/container/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/dispatcher/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/protocol/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/support/*.rb') + rdoc.rdoc_files.include('vendor/rails/activesupport/README') + rdoc.rdoc_files.include('vendor/rails/activesupport/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/activesupport/lib/active_support/**/*.rb') + } + + plugins = FileList['vendor/plugins/**'].collect { |plugin| File.basename(plugin) } + + desc "Generate documation for all installed plugins" + task :plugins => plugins.collect { |plugin| "doc:plugins:#{plugin}" } + + desc "Remove plugin documentation" + task :clobber_plugins do + rm_rf 'doc/plugins' rescue nil + end + + namespace :plugins do + # Define doc tasks for each plugin + plugins.each do |plugin| + task(plugin => :environment) do + plugin_base = "vendor/plugins/#{plugin}" + options = [] + files = Rake::FileList.new + options << "-o doc/plugins/#{plugin}" + options << "--title '#{plugin.titlecase} Plugin Documentation'" + options << '--line-numbers' << '--inline-source' + options << '-T html' + + files.include("#{plugin_base}/lib/**/*.rb") + if File.exists?("#{plugin_base}/README") + files.include("#{plugin_base}/README") + options << "--main '#{plugin_base}/README'" + end + files.include("#{plugin_base}/CHANGELOG") if File.exists?("#{plugin_base}/CHANGELOG") + + options << files.to_s + + sh %(rdoc #{options * ' '}) + end + end + end +end \ No newline at end of file diff --git a/vendor/rails/railties/lib/tasks/framework.rake b/vendor/rails/railties/lib/tasks/framework.rake new file mode 100644 index 00000000..bdcc2666 --- /dev/null +++ b/vendor/rails/railties/lib/tasks/framework.rake @@ -0,0 +1,114 @@ +namespace :rails do + namespace :freeze do + desc "Lock this application to the current gems (by unpacking them into vendor/rails)" + task :gems do + deps = %w(actionpack activerecord actionmailer activesupport actionwebservice) + require 'rubygems' + Gem.manage_gems + + rails = (version = ENV['VERSION']) ? + Gem.cache.search('rails', "= #{version}").first : + Gem.cache.search('rails').sort_by { |g| g.version }.last + + version ||= rails.version + + unless rails + puts "No rails gem #{version} is installed. Do 'gem list rails' to see what you have available." + exit + end + + puts "Freezing to the gems for Rails #{rails.version}" + rm_rf "vendor/rails" + mkdir_p "vendor/rails" + + chdir("vendor/rails") do + rails.dependencies.select { |g| deps.include? g.name }.each do |g| + Gem::GemRunner.new.run(["unpack", "-v", "#{g.version_requirements}", "#{g.name}"]) + mv(Dir.glob("#{g.name}*").first, g.name) + end + + Gem::GemRunner.new.run(["unpack", "-v", "=#{version}", "rails"]) + FileUtils.mv(Dir.glob("rails*").first, "railties") + end + end + + desc "Lock to latest Edge Rails or a specific revision with REVISION=X (ex: REVISION=4021) or a tag with TAG=Y (ex: TAG=rel_1-1-0)" + task :edge do + $verbose = false + `svn --version` rescue nil + unless !$?.nil? && $?.success? + $stderr.puts "ERROR: Must have subversion (svn) available in the PATH to lock this application to Edge Rails" + exit 1 + end + + rm_rf "vendor/rails" + mkdir_p "vendor/rails" + + svn_root = "http://dev.rubyonrails.org/svn/rails/" + + if ENV['TAG'] + rails_svn = "#{svn_root}/tags/#{ENV['TAG']}" + touch "vendor/rails/TAG_#{ENV['TAG']}" + else + rails_svn = "#{svn_root}/trunk" + + if ENV['REVISION'].nil? + ENV['REVISION'] = /^r(\d+)/.match(%x{svn -qr HEAD log #{svn_root}})[1] + puts "REVISION not set. Using HEAD, which is revision #{ENV['REVISION']}." + end + + touch "vendor/rails/REVISION_#{ENV['REVISION']}" + end + + for framework in %w( railties actionpack activerecord actionmailer activesupport actionwebservice ) + system "svn export #{rails_svn}/#{framework} vendor/rails/#{framework}" + (ENV['REVISION'] ? " -r #{ENV['REVISION']}" : "") + end + end + end + + desc "Unlock this application from freeze of gems or edge and return to a fluid use of system gems" + task :unfreeze do + rm_rf "vendor/rails" + end + + desc "Update both configs, scripts and public/javascripts from Rails" + task :update => [ "update:scripts", "update:javascripts", "update:configs" ] + + namespace :update do + desc "Add new scripts to the application script/ directory" + task :scripts do + local_base = "script" + edge_base = "#{File.dirname(__FILE__)}/../../bin" + + local = Dir["#{local_base}/**/*"].reject { |path| File.directory?(path) } + edge = Dir["#{edge_base}/**/*"].reject { |path| File.directory?(path) } + + edge.each do |script| + base_name = script[(edge_base.length+1)..-1] + next if base_name == "rails" + next if local.detect { |path| base_name == path[(local_base.length+1)..-1] } + if !File.directory?("#{local_base}/#{File.dirname(base_name)}") + mkdir_p "#{local_base}/#{File.dirname(base_name)}" + end + install script, "#{local_base}/#{base_name}", :mode => 0755 + end + end + + desc "Update your javascripts from your current rails install" + task :javascripts do + require 'railties_path' + project_dir = RAILS_ROOT + '/public/javascripts/' + scripts = Dir[RAILTIES_PATH + '/html/javascripts/*.js'] + scripts.reject!{|s| File.basename(s) == 'application.js'} if File.exists?(project_dir + 'application.js') + FileUtils.cp(scripts, project_dir) + end + + desc "Update boot/config.rb from your current rails install" + task :configs do + require 'railties_path' + project_dir = RAILS_ROOT + '/public/javascripts/' + scripts = Dir[RAILTIES_PATH + '/html/javascripts/*.js'] + FileUtils.cp(RAILTIES_PATH + '/environments/boot.rb', RAILS_ROOT + '/config/boot.rb') + end + end +end diff --git a/vendor/rails/railties/lib/tasks/log.rake b/vendor/rails/railties/lib/tasks/log.rake new file mode 100644 index 00000000..6e133469 --- /dev/null +++ b/vendor/rails/railties/lib/tasks/log.rake @@ -0,0 +1,9 @@ +namespace :log do + desc "Truncates all *.log files in log/ to zero bytes" + task :clear do + FileList["log/*.log"].each do |log_file| + f = File.open(log_file, "w") + f.close + end + end +end diff --git a/vendor/rails/railties/lib/tasks/misc.rake b/vendor/rails/railties/lib/tasks/misc.rake new file mode 100644 index 00000000..02ba8860 --- /dev/null +++ b/vendor/rails/railties/lib/tasks/misc.rake @@ -0,0 +1,4 @@ +task :default => :test +task :environment do + require(File.join(RAILS_ROOT, 'config', 'environment')) +end \ No newline at end of file diff --git a/vendor/rails/railties/lib/tasks/pre_namespace_aliases.rake b/vendor/rails/railties/lib/tasks/pre_namespace_aliases.rake new file mode 100644 index 00000000..46215e7c --- /dev/null +++ b/vendor/rails/railties/lib/tasks/pre_namespace_aliases.rake @@ -0,0 +1,46 @@ +# clear +task :clear_logs => "log:clear" + +# test +task :recent => "test:recent" +task :test_units => "test:units" +task :test_functional => "test:functionals" +task :test_plugins => "test:plugins" + + +# doc +task :appdoc => "doc:app" +task :apidoc => "doc:rails" +task :plugindoc => "doc:plugins" +task :clobber_plugindoc => "doc:clobber_plugins" + +FileList['vendor/plugins/**'].collect { |plugin| File.basename(plugin) }.each do |plugin| + task :"#{plugin}_plugindoc" => "doc:plugins:#{plugin}" +end + + +# rails +task :freeze_gems => "rails:freeze:gems" +task :freeze_edge => "rails:freeze:edge" +task :unfreeze_rails => "rails:unfreeze" +task :add_new_scripts => "rails:update:scripts" +task :update_javascripts => "rails:update:javascripts" + + +# db +task :migrate => "db:migrate" +task :load_fixtures => "db:fixtures:load" + +task :db_schema_dump => "db:schema:dump" +task :db_schema_import => "db:schema:load" + +task :db_structure_dump => "db:structure:dump" + +task :purge_test_database => "db:test:purge" +task :clone_schema_to_test => "db:test:clone" +task :clone_structure_to_test => "db:test:clone_structure" +task :prepare_test_database => "db:test:prepare" + +task :create_sessions_table => "db:sessions:create" +task :drop_sessions_table => "db:sessions:drop" +task :purge_sessions_table => "db:sessions:recreate" diff --git a/vendor/rails/railties/lib/tasks/rails.rb b/vendor/rails/railties/lib/tasks/rails.rb new file mode 100644 index 00000000..48a9d67b --- /dev/null +++ b/vendor/rails/railties/lib/tasks/rails.rb @@ -0,0 +1,8 @@ +$VERBOSE = nil + +# Load Rails rakefile extensions +Dir["#{File.dirname(__FILE__)}/*.rake"].each { |ext| load ext } + +# Load any custom rakefile extensions +Dir["./lib/tasks/**/*.rake"].sort.each { |ext| load ext } +Dir["./vendor/plugins/*/tasks/**/*.rake"].sort.each { |ext| load ext } \ No newline at end of file diff --git a/vendor/rails/railties/lib/tasks/statistics.rake b/vendor/rails/railties/lib/tasks/statistics.rake new file mode 100644 index 00000000..87b89e51 --- /dev/null +++ b/vendor/rails/railties/lib/tasks/statistics.rake @@ -0,0 +1,17 @@ +STATS_DIRECTORIES = [ + %w(Helpers app/helpers), + %w(Controllers app/controllers), + %w(APIs app/apis), + %w(Components components), + %w(Functional\ tests test/functional), + %w(Models app/models), + %w(Unit\ tests test/unit), + %w(Libraries lib/), + %w(Integration\ tests test/integration) +].collect { |name, dir| [ name, "#{RAILS_ROOT}/#{dir}" ] }.select { |name, dir| File.directory?(dir) } + +desc "Report code statistics (KLOCs, etc) from the application" +task :stats do + require 'code_statistics' + CodeStatistics.new(*STATS_DIRECTORIES).to_s +end diff --git a/vendor/rails/railties/lib/tasks/testing.rake b/vendor/rails/railties/lib/tasks/testing.rake new file mode 100644 index 00000000..c735c8db --- /dev/null +++ b/vendor/rails/railties/lib/tasks/testing.rake @@ -0,0 +1,102 @@ +TEST_CHANGES_SINCE = Time.now - 600 + +# Look up tests for recently modified sources. +def recent_tests(source_pattern, test_path, touched_since = 10.minutes.ago) + FileList[source_pattern].map do |path| + if File.mtime(path) > touched_since + test = "#{test_path}/#{File.basename(path, '.rb')}_test.rb" + test if File.exists?(test) + end + end.compact +end + + +# Recreated here from ActiveSupport because :uncommitted needs it before Rails is available +module Kernel + def silence_stderr + old_stderr = STDERR.dup + STDERR.reopen(RUBY_PLATFORM =~ /mswin/ ? 'NUL:' : '/dev/null') + STDERR.sync = true + yield + ensure + STDERR.reopen(old_stderr) + end +end + +desc 'Test all units and functionals' +task :test do + Rake::Task["test:units"].invoke rescue got_error = true + Rake::Task["test:functionals"].invoke rescue got_error = true + + if File.exist?("test/integration") + Rake::Task["test:integration"].invoke rescue got_error = true + end + + raise "Test failures" if got_error +end + +namespace :test do + desc 'Test recent changes' + Rake::TestTask.new(:recent => "db:test:prepare") do |t| + since = TEST_CHANGES_SINCE + touched = FileList['test/**/*_test.rb'].select { |path| File.mtime(path) > since } + + recent_tests('app/models/*.rb', 'test/unit', since) + + recent_tests('app/controllers/*.rb', 'test/functional', since) + + t.libs << 'test' + t.verbose = true + t.test_files = touched.uniq + end + + desc 'Test changes since last checkin (only Subversion)' + Rake::TestTask.new(:uncommitted => "db:test:prepare") do |t| + def t.file_list + changed_since_checkin = silence_stderr { `svn status` }.map { |path| path.chomp[7 .. -1] } + + models = changed_since_checkin.select { |path| path =~ /app\/models\/.*\.rb/ } + controllers = changed_since_checkin.select { |path| path =~ /app\/controllers\/.*\.rb/ } + + unit_tests = models.map { |model| "test/unit/#{File.basename(model, '.rb')}_test.rb" } + functional_tests = controllers.map { |controller| "test/functional/#{File.basename(controller, '.rb')}_test.rb" } + + unit_tests.uniq + functional_tests.uniq + end + + t.libs << 'test' + t.verbose = true + end + + desc "Run the unit tests in test/unit" + Rake::TestTask.new(:units => "db:test:prepare") do |t| + t.libs << "test" + t.pattern = 'test/unit/**/*_test.rb' + t.verbose = true + end + + desc "Run the functional tests in test/functional" + Rake::TestTask.new(:functionals => "db:test:prepare") do |t| + t.libs << "test" + t.pattern = 'test/functional/**/*_test.rb' + t.verbose = true + end + + desc "Run the integration tests in test/integration" + Rake::TestTask.new(:integration => "db:test:prepare") do |t| + t.libs << "test" + t.pattern = 'test/integration/**/*_test.rb' + t.verbose = true + end + + desc "Run the plugin tests in vendor/plugins/**/test (or specify with PLUGIN=name)" + Rake::TestTask.new(:plugins => :environment) do |t| + t.libs << "test" + + if ENV['PLUGIN'] + t.pattern = "vendor/plugins/#{ENV['PLUGIN']}/test/**/*_test.rb" + else + t.pattern = 'vendor/plugins/**/test/**/*_test.rb' + end + + t.verbose = true + end +end diff --git a/vendor/rails/railties/lib/tasks/tmp.rake b/vendor/rails/railties/lib/tasks/tmp.rake new file mode 100644 index 00000000..6ba5b114 --- /dev/null +++ b/vendor/rails/railties/lib/tasks/tmp.rake @@ -0,0 +1,30 @@ +namespace :tmp do + desc "Clear session, cache, and socket files from tmp/" + task :clear => [ "tmp:sessions:clear", "tmp:cache:clear", "tmp:sockets:clear"] + + desc "Creates tmp directories for sessions, cache, and sockets" + task :create do + FileUtils.mkdir_p(%w( tmp/sessions tmp/cache tmp/sockets )) + end + + namespace :sessions do + desc "Clears all files in tmp/sessions" + task :clear do + FileUtils.rm(Dir['tmp/sessions/[^.]*']) + end + end + + namespace :cache do + desc "Clears all files and directories in tmp/cache" + task :clear do + FileUtils.rm_rf(Dir['tmp/cache/[^.]*']) + end + end + + namespace :sockets do + desc "Clears all files in tmp/sockets" + task :clear do + FileUtils.rm(Dir['tmp/sockets/[^.]*']) + end + end +end \ No newline at end of file diff --git a/vendor/rails/railties/lib/test_help.rb b/vendor/rails/railties/lib/test_help.rb new file mode 100644 index 00000000..dcf47364 --- /dev/null +++ b/vendor/rails/railties/lib/test_help.rb @@ -0,0 +1,18 @@ +require 'application' + +# Make double-sure the RAILS_ENV is set to test, +# so fixtures are loaded to the right database +silence_warnings { RAILS_ENV = "test" } + +require 'test/unit' +require 'active_record/fixtures' +require 'action_controller/test_process' +require 'action_controller/integration' +require 'action_web_service/test_invoke' +require 'breakpoint' + +Test::Unit::TestCase.fixture_path = RAILS_ROOT + "/test/fixtures/" + +def create_fixtures(*table_names) + Fixtures.create_fixtures(RAILS_ROOT + "/test/fixtures", table_names) +end diff --git a/vendor/rails/railties/lib/webrick_server.rb b/vendor/rails/railties/lib/webrick_server.rb new file mode 100644 index 00000000..679fd1f6 --- /dev/null +++ b/vendor/rails/railties/lib/webrick_server.rb @@ -0,0 +1,168 @@ +# Donated by Florian Gross + +require 'webrick' +require 'cgi' +require 'stringio' + +include WEBrick + +ABSOLUTE_RAILS_ROOT = File.expand_path(RAILS_ROOT) + +class CGI #:nodoc: + def stdinput + @stdin || $stdin + end + + def env_table + @env_table || ENV + end + + def initialize(type = "query", table = nil, stdin = nil) + @env_table, @stdin = table, stdin + + if defined?(MOD_RUBY) && !ENV.key?("GATEWAY_INTERFACE") + Apache.request.setup_cgi_env + end + + extend QueryExtension + @multipart = false + if defined?(CGI_PARAMS) + warn "do not use CGI_PARAMS and CGI_COOKIES" + @params = CGI_PARAMS.dup + @cookies = CGI_COOKIES.dup + else + initialize_query() # set @params, @cookies + end + @output_cookies = nil + @output_hidden = nil + end +end + +# A custom dispatch servlet for use with WEBrick. It dispatches requests +# (using the Rails Dispatcher) to the appropriate controller/action. By default, +# it restricts WEBrick to a managing a single Rails request at a time, but you +# can change this behavior by setting ActionController::Base.allow_concurrency +# to true. +class DispatchServlet < WEBrick::HTTPServlet::AbstractServlet + REQUEST_MUTEX = Mutex.new + + # Start the WEBrick server with the given options, mounting the + # DispatchServlet at /. + def self.dispatch(options = {}) + Socket.do_not_reverse_lookup = true # patch for OS X + + params = { :Port => options[:port].to_i, + :ServerType => options[:server_type], + :BindAddress => options[:ip] } + params[:MimeTypes] = options[:mime_types] if options[:mime_types] + + server = WEBrick::HTTPServer.new(params) + server.mount('/', DispatchServlet, options) + + trap("INT") { server.shutdown } + + require File.join(@server_options[:server_root], "..", "config", "environment") unless defined?(RAILS_ROOT) + require "dispatcher" + + server.start + end + + def initialize(server, options) #:nodoc: + @server_options = options + @file_handler = WEBrick::HTTPServlet::FileHandler.new(server, options[:server_root]) + Dir.chdir(ABSOLUTE_RAILS_ROOT) + super + end + + def service(req, res) #:nodoc: + unless handle_file(req, res) + begin + REQUEST_MUTEX.lock unless ActionController::Base.allow_concurrency + unless handle_dispatch(req, res) + raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." + end + ensure + unless ActionController::Base.allow_concurrency + REQUEST_MUTEX.unlock if REQUEST_MUTEX.locked? + end + end + end + end + + def handle_file(req, res) #:nodoc: + begin + req = req.dup + path = req.path.dup + + # Add .html if the last path piece has no . in it + path << '.html' if path != '/' && (%r{(^|/)[^./]+$} =~ path) + path.gsub!('+', ' ') # Unescape + since FileHandler doesn't do so. + + req.instance_variable_set(:@path_info, path) # Set the modified path... + + @file_handler.send(:service, req, res) + return true + rescue HTTPStatus::PartialContent, HTTPStatus::NotModified => err + res.set_error(err) + return true + rescue => err + return false + end + end + + def handle_dispatch(req, res, origin = nil) #:nodoc: + data = StringIO.new + Dispatcher.dispatch( + CGI.new("query", create_env_table(req, origin), StringIO.new(req.body || "")), + ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, + data + ) + + header, body = extract_header_and_body(data) + + set_charset(header) + assign_status(res, header) + res.cookies.concat(header.delete('set-cookie') || []) + header.each { |key, val| res[key] = val.join(", ") } + + res.body = body + return true + rescue => err + p err, err.backtrace + return false + end + + private + def create_env_table(req, origin) + env = req.meta_vars.clone + env.delete "SCRIPT_NAME" + env["QUERY_STRING"] = req.request_uri.query + env["REQUEST_URI"] = origin if origin + return env + end + + def extract_header_and_body(data) + data.rewind + data = data.read + + raw_header, body = *data.split(/^[\xd\xa]+/on, 2) + header = WEBrick::HTTPUtils::parse_header(raw_header) + + return header, body + end + + def set_charset(header) + ct = header["content-type"] + if ct.any? { |x| x =~ /^text\// } && ! ct.any? { |x| x =~ /charset=/ } + ch = @server_options[:charset] || "UTF-8" + ct.find { |x| x =~ /^text\// } << ("; charset=" + ch) + end + end + + def assign_status(res, header) + if /^(\d+)/ =~ header['status'][0] + res.status = $1.to_i + header.delete('status') + end + end +end diff --git a/vendor/rails/railties/test/dispatcher_test.rb b/vendor/rails/railties/test/dispatcher_test.rb new file mode 100644 index 00000000..41d08e22 --- /dev/null +++ b/vendor/rails/railties/test/dispatcher_test.rb @@ -0,0 +1,92 @@ +$:.unshift File.dirname(__FILE__) + "/../lib" +$:.unshift File.dirname(__FILE__) + "/../../actionpack/lib" +$:.unshift File.dirname(__FILE__) + "/../../actionmailer/lib" + +require 'test/unit' +require 'stringio' +require 'cgi' + +require 'dispatcher' +require 'action_controller' +require 'action_mailer' + +ACTION_MAILER_DEF = < 'table' + assert_response :success + end + + def test_rails_info_properties_error_rendered_for_non_local_request + Rails::InfoController.local_request = false + get :properties + assert_tag :tag => 'p' + assert_response 500 + end +end diff --git a/vendor/rails/railties/test/rails_info_test.rb b/vendor/rails/railties/test/rails_info_test.rb new file mode 100644 index 00000000..926f254c --- /dev/null +++ b/vendor/rails/railties/test/rails_info_test.rb @@ -0,0 +1,92 @@ +$:.unshift File.dirname(__FILE__) + "/../lib" +$:.unshift File.dirname(__FILE__) + "/../../activesupport/lib" + +require 'test/unit' +require 'active_support' +require 'rails_info' + +class InfoTest < Test::Unit::TestCase + def setup + Rails.send :remove_const, :Info + silence_warnings { load 'rails_info.rb' } + end + + def test_edge_rails_revision_not_set_when_svn_info_is_empty + Rails::Info.property 'Test that this will not be defined' do + Rails::Info.edge_rails_revision '' + end + assert !property_defined?('Test that this will not be defined') + end + + def test_edge_rails_revision_extracted_from_svn_info + Rails::Info.property 'Test Edge Rails revision' do + Rails::Info.edge_rails_revision <<-EOS +Path: . +URL: http://www.rubyonrails.com/svn/rails/trunk +Repository UUID: 5ecf4fe2-1ee6-0310-87b1-e25e094e27de +Revision: 2881 +Node Kind: directory +Schedule: normal +Last Changed Author: sam +Last Changed Rev: 2881 +Last Changed Date: 2005-11-04 21:04:41 -0600 (Fri, 04 Nov 2005) +Properties Last Updated: 2005-10-28 19:30:00 -0500 (Fri, 28 Oct 2005) + +EOS + end + + assert_property 'Test Edge Rails revision', '2881' + end + + def test_property_with_block_swallows_exceptions_and_ignores_property + assert_nothing_raised do + Rails::Info.module_eval do + property('Bogus') {raise} + end + end + assert !property_defined?('Bogus') + end + + def test_property_with_string + Rails::Info.module_eval do + property 'Hello', 'World' + end + assert_property 'Hello', 'World' + end + + def test_property_with_block + Rails::Info.module_eval do + property('Goodbye') {'World'} + end + assert_property 'Goodbye', 'World' + end + + def test_component_version + assert_property 'Active Support version', ActiveSupport::VERSION::STRING + end + +protected + def svn_info=(info) + Rails::Info.module_eval do + class << self + def svn_info + info + end + end + end + end + + def properties + Rails::Info.properties + end + + def property_defined?(property_name) + properties.names.include? property_name + end + + def assert_property(property_name, value) + raise "Property #{property_name.inspect} not defined" unless + property_defined? property_name + assert_equal value, properties.value_for(property_name) + end +end diff --git a/vendor/rails/railties/test/webrick_dispatcher_test.rb b/vendor/rails/railties/test/webrick_dispatcher_test.rb new file mode 100644 index 00000000..7f5be65f --- /dev/null +++ b/vendor/rails/railties/test/webrick_dispatcher_test.rb @@ -0,0 +1,27 @@ +#!/bin/env ruby + +$:.unshift(File.dirname(__FILE__) + "/../lib") + +require 'test/unit' +require 'webrick_server' + +class ParseUriTest < Test::Unit::TestCase + + def test_parse_uri_proper_behavior + assert_equal({:id=>"1", :controller=>"forum", :action=>"index"}, DispatchServlet.parse_uri('/forum/index/1')) + assert_equal({:controller=>"forum", :action=>"index"}, DispatchServlet.parse_uri('/forum')) + assert_equal({:controller=>"forum", :action=>"index"}, DispatchServlet.parse_uri('/forum/index')) + assert_equal({:controller=>"forum", :action=>"index"}, DispatchServlet.parse_uri('/forum/')) + assert_equal({:action=>"index", :module=>"admin", :controller=>"forum"}, DispatchServlet.parse_uri('/admin/forum/')) + end + + def test_parse_uri_failures + assert_equal false, DispatchServlet.parse_uri('/forum/index/1/') + assert_equal false, DispatchServlet.parse_uri('/') + assert_equal false, DispatchServlet.parse_uri('a') + assert_equal false, DispatchServlet.parse_uri('/forum//') + assert_equal false, DispatchServlet.parse_uri('/+forum/') + assert_equal false, DispatchServlet.parse_uri('forum/') + end + +end diff --git a/vendor/rails/release.rb b/vendor/rails/release.rb new file mode 100755 index 00000000..b936651e --- /dev/null +++ b/vendor/rails/release.rb @@ -0,0 +1,25 @@ +#!/usr/local/bin/ruby + +VERSION = ARGV.first +PACKAGES = %w( activesupport activerecord actionpack actionmailer actionwebservice ) + +# Checkout source +`rm -rf release && svn export http://dev.rubyonrails.org/svn/rails/branches/stable release` + +# Create Rails packages +`cd release/railties && rake template=jamis package` + +# Upload documentation +`cd release/rails/doc/api && scp -r * davidhh@wrath.rubyonrails.com:public_html/api` + +# Upload packages +(PACKAGES + %w(railties)).each do |p| + `cd release/#{p} && echo "Releasing #{p}" && rake release` +end + +# Upload rails tgz/zip +`rubyforge add_release rails rails 'REL #{VERSION}' release/rails-#{VERSION}.tgz` +`rubyforge add_release rails rails 'REL #{VERSION}' release/rails-#{VERSION}.zip` + +# Create SVN tag +puts "Remeber to create SVN tag"