diff options
Diffstat (limited to 'actionpack')
105 files changed, 9547 insertions, 0 deletions
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG new file mode 100644 index 0000000000..a422af3f0e --- /dev/null +++ b/actionpack/CHANGELOG @@ -0,0 +1,738 @@ +*CVS* + +* Upgraded to Builder 1.2.1 + +* Added :module as an alias for :controller_prefix to url_for and friends, so you can do redirect_to(:module => "shop", :controller => "purchases") + and go to /shop/purchases/ + +* Added support for controllers in modules through @params["module"]. + +* Added reloading for dependencies under cached environments like FastCGI and mod_ruby. This makes it possible to use those environments for development. + This is turned on by default, but can be turned off with ActionController::Base.reload_dependencies = false in production environments. + + NOTE: This will only have an effect if you use the new model, service, and observer class methods to mark dependencies. All libraries loaded through + require will be "forever" cached. You can, however, use ActionController::Base.load_or_require("library") to get this behavior outside of the new + dependency style. + +* Added that controllers will automatically require their own helper if possible. So instead of doing: + + class MsgController < AbstractApplicationController + helper :msg + end + + ...you can just do: + + class MsgController < AbstractApplicationController + end + +* Added dependencies_on(layer) to query the dependencies of a controller. Examples: + + MsgController.dependencies_on(:model) # => [ :post, :comment, :attachment ] + MsgController.dependencies_on(:service) # => [ :notification_service ] + MsgController.dependencies_on(:observer) # => [ :comment_observer ] + +* Added a new dependency model with the class methods model, service, and observer. Example: + + class MsgController < AbstractApplicationController + model :post, :comment, :attachment + service :notification_service + observer :comment_observer + end + + These new "keywords" remove the need for explicitly calling 'require' in most cases. The observer method even instantiates the + observer as well as requiring it. + +* Fixed that link_to would escape & in the url again after url_for already had done so + +*0.9.5* (28) + +* Added helper_method to designate that a given private or protected method you should available as a helper in the view. [bitsweat] + +* Fixed assert_rendered_file so it actually verifies if that was the rendered file [htonl] + +* Added the option for sharing partial spacer templates just like partials themselves [radsaq] + +* Fixed that Russia was named twice in country_select [alexey] + +* Fixed request_origin to use remote_ip instead of remote_addr [bitsweat] + +* Fixed link_to breakage when nil was passed for html_options [alexey] + +* Fixed redirect_to on a virtual server setup with apache with a port other than the default where it would forget the port number [seanohalpin] + +* Fixed that auto-loading webrick on Windows would cause file uploads to fail [bitsweat] + +* Fixed issues with sending files on WEBrick by setting the proper binmode [bitsweat] + +* Added send_data as an alternative to send_file when the stream is not read off the filesystem but from a database or generated live [bitsweat] + +* Added a new way to include helpers that doesn't require the include hack and can go without the explicit require. [bitsweat] + + Before: + + module WeblogHelper + def self.append_features(controller) #:nodoc: + controller.ancestors.include?(ActionController::Base) ? controller.add_template_helper(self) : super + end + end + + require 'weblog_helper' + class WeblogController < ActionController::Base + include WeblogHelper + end + + After: + + module WeblogHelper + end + + class WeblogController < ActionController::Base + helper :weblog + end + +* Added a default content-type of "text/xml" to .rxml renders [Ryan Platte] + +* Fixed that when /controller/index was requested by the browser, url_for would generates wrong URLs [Ryan Platte] + +* Fixed a bug that would share cookies between users when using FastCGI and mod_ruby [The Robot Co-op] + +* Added an optional third hash parameter to the process method in functional tests that takes the session data to be used [alexey] + +* Added UrlHelper#mail_to to make it easier to create mailto: style ahrefs + +* Added better error messages for layouts declared with the .rhtml extension (which they shouldn't) [geech] + +* Added another case to DateHelper#distance_in_minutes to return "less than a minute" instead of "0 minutes" and "1 minute" instead of "1 minutes" + +* Added a hidden field to checkboxes generated with FormHelper#check_box that will make sure that the unchecked value (usually 0) + is sent even if the checkbox is not checked. This relieves the controller from doing custom checking if the the checkbox wasn't + checked. BEWARE: This might conflict with your run-on-the-mill work-around code. [Tobias Luetke] + +* Fixed error_message_on to just use the first if more than one error had been added [marcel] + +* Fixed that URL rewriting with /controller/ was working but /controller was not and that you couldn't use :id on index [geech] + +* Fixed a bug with link_to where the :confirm option wouldn't be picked up if the link was a straight url instead of an option hash + +* Changed scaffolding of forms to use <label> tags instead of <b> to please W3C [evl] + +* Added DateHelper#distance_of_time_in_words_to_now(from_time) that works like distance_of_time_in_words, + but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>. + +* Added assert_flash_equal(expected, key, message), assert_session_equal(expected, key, message), + assert_assigned_equal(expected, key, message) to test the contents of flash, session, and template assigns. + +* Improved the failure report on assert_success when the action triggered a redirection [alexey]. + +* Added "markdown" to accompany "textilize" as a TextHelper method for converting text to HTML using the Markdown syntax. + BlueCloth must be installed in order for this method to become available. + +* Made sure that an active session exists before we attempt to delete it [Samuel] + +* Changed link_to with Javascript confirmation to use onclick instead of onClick for XHTML validity [Scott Barron] + + +*0.9.0 (43)* + +* Added support for Builder-based templates for files with the .rxml extension. These new templates are an alternative to ERb that + are especially useful for generating XML content, such as this RSS example from Basecamp: + + xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do + xml.channel do + xml.title(@feed_title) + xml.link(@url) + xml.description "Basecamp: Recent items" + xml.language "en-us" + xml.ttl "40" + + for item in @recent_items + xml.item do + xml.title(item_title(item)) + xml.description(item_description(item)) if item_description(item) + xml.pubDate(item_pubDate(item)) + xml.guid(@person.firm.account.url + @recent_items.url(item)) + xml.link(@person.firm.account.url + @recent_items.url(item)) + + xml.tag!("dc:creator", item.author_name) if item_has_creator?(item) + end + end + end + end + + ...which will generate something like: + + <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"> + <channel> + <title>Web Site Redesign</title> + <link>http://www.basecamphq.com/clients/travelcenter/1/</link> + <description>Basecamp: Recent items</description> + <language>en-us</language> + <ttl>40</ttl> + <item> + <title>Post: don't you know</title> + <description>&lt;p&gt;deeper and down&lt;/p&gt;</description> + <pubDate>Fri, 20 Aug 2004 21:13:50 CEST</pubDate> + <guid>http://www.basecamphq.com/clients/travelcenter/1/msg/assets/96976/comments</guid> + <link>http://www.basecamphq.com/clients/travelcenter/1/msg/assets/96976/comments</link> + <dc:creator>David H. Heinemeier</dc:creator> + </item> + <item> + <title>Milestone completed: Design Comp 2</title> + <pubDate>Mon, 9 Aug 2004 14:42:06 CEST</pubDate> + <guid>http://www.basecamphq.com/clients/travelcenter/1/milestones/#49</guid> + <link>http://www.basecamphq.com/clients/travelcenter/1/milestones/#49</link> + </item> + </channel> + </rss> + + The "xml" local variable is automatically available in .rxml templates. You construct the template by calling a method with the name + of the tag you want. Options for the tag can be specified as a hash parameter to that method. + + Builder-based templates can be mixed and matched with the regular ERb ones. The only thing that differentiates them is the extension. + No new methods have been added to the public interface to handle them. + + Action Pack ships with a version of Builder, but it will use the RubyGems version if you have one installed. + + Read more about Builder on: http://onestepback.org/index.cgi/Tech/Ruby/StayingSimple.rdoc + + [Builder is created by Jim Weirich] + +* Added much improved support for functional testing [what-a-day]. + + # Old style + def test_failing_authenticate + @request.request_uri = "/login/authenticate" + @request.action = "authenticate" + @request.request_parameters["user_name"] = "nop" + @request.request_parameters["password"] = "" + + response = LoginController.process_test(@request) + + assert_equal "The username and/or password you entered is invalid.", response.session["flash"]["alert"] + assert_equal "http://37signals.basecamp.com/login/", response.headers["location"] + end + + # New style + def test_failing_authenticate + process :authenticate, "user_name" => "nop", "password" => "" + assert_flash_has 'alert' + assert_redirected_to :action => "index" + end + + See a full example on http://codepaste.org/view/paste/334 + +* Increased performance by up to 100% with a revised cookie class that fixes the performance problems with the + default one that ships with 1.8.1 and below. It replaces the inheritance on SimpleDelegator with DelegateClass(Array) + following the suggestion from Matz on: + http://groups.google.com/groups?th=e3a4e68ba042f842&seekm=c3sioe%241qvm%241%40news.cybercity.dk#link14 + +* Added caching for compiled ERb templates. On Basecamp, it gave between 8.5% and 71% increase in performance [Andreas Schwarz]. + +* Added implicit counter variable to render_collection_of_partials [Marcel]. From the docs: + + <%= render_collection_of_partials "ad", @advertisements %> + + This will render "advertiser/_ad.rhtml" and pass the local variable +ad+ to the template for display. An iteration counter + will automatically be made available to the template with a name of the form +partial_name_counter+. In the case of the + example above, the template would be fed +ad_counter+. + +* Fixed problems with two sessions being maintained on reset_session that would particularly screw up ActiveRecordStore. + +* Fixed reset_session to start an entirely new session instead of merely deleting the old. So you can now safely access @session + after calling reset_ression and expect it to work. + +* Added @request.get?, @request.post?, @request.put?, @request.delete? as convenience query methods for @request.method [geech] + +* Added @request.method that'll return a symbol representing the HTTP method, such as :get, :post, :put, :delete [geech] + +* Changed @request.remote_ip and @request.host to work properly even when a proxy is in front of the application [geech] + +* Added JavaScript confirm feature to link_to. Documentation: + + The html_options have a special feature for creating javascript confirm alerts where if you pass + :confirm => 'Are you sure?', the link will be guarded with a JS popup asking that question. + If the user accepts, the link is processed, otherwise not. + +* Added link_to_unless_current as a UrlHelper method [Sam Stephenson]. Documentation: + + Creates a link tag of the given +name+ using an URL created by the set of +options+, unless the current + controller, action, and id are the same as the link's, in which case only the name is returned (or the + given block is yielded, if one exists). This is useful for creating link bars where you don't want to link + to the page currently being viewed. + +* Fixed that UrlRewriter (the driver for url_for, link_to, etc) would blow up when the anchor was an integer [alexey] + +* Added that layouts defined with no directory defaults to layouts. So layout "weblog/standard" will use + weblog/standard (as always), but layout "standard" will use layouts/standard. + +* Fixed that partials (or any template starting with an underscore) was publically viewable [Marten] + +* Added HTML escaping to text_area helper. + +* Added :overwrite_params to url_for and friends to keep the parameters as they were passed to the current action and only overwrite a subset. + The regular :params will clear the slate so you need to manually add in existing parameters if you want to reuse them. [raphinou] + +* Fixed scaffolding problem with composite named objects [Moo Jester] + +* Added the possibility for shared partials. Example: + + <%= render_partial "advertisement/ad", ad %> + + This will render the partial "advertisement/_ad.rhtml" regardless of which controller this is being called from. + + [Jacob Fugal] + +* Fixed crash when encountering forms that have empty-named fields [James Prudente] + +* Added check_box form helper method now accepts true/false as well as 1/0 [what-a-day] + +* Fixed the lacking creation of all directories with install.rb [Dave Steinberg] + +* Fixed that date_select returns valid XHTML selected options [Andreas Schwarz] + +* Fixed referencing an action with the same name as a controller in url_for [what-a-day] + +* Fixed the destructive nature of Base#attributes= on the argument [Kevin Watt] + +* Changed ActionControllerError to decent from StandardError instead of Exception. It can now be caught by a generic rescue. + +* Added SessionRestoreError that is raised when a session being restored holds objects where there is no class available. + +* Added block as option for inline filters. So what used to be written as: + + before_filter Proc { |controller| return false if controller.params["stop_action"] } + + ...can now be as: + + before_filter { |controller| return false if controller.params["stop_action"] } + + [Jeremy Kemper] + +* Made the following methods public (was protected): url_for, controller_class_name, controller_name, action_name + This makes it easier to write filters without cheating around the encapsulation with send. + +* ActionController::Base#reset_session now sticks even if you access @session afterwards [Kent Sibilev] + +* Improved the exception logging so the log file gets almost as much as in-browser debugging. + +* Changed base class setup from AbstractTemplate/ERbTemplate to ActionView::Base. This change should be harmless unless you were + accessing Action View directly in which case you now need to reference the Base class.\ + +* Added that render_collection_of_partials returns nil if the collection is empty. This makes showing a “no items” message easier. + For example: <%= render_collection_of_partials("message", @messages) || "No messages found." %> [Sam Stephenson] + +* Added :month_before_year as an option to date_select to get the month select before the year. Especially useful for credit card forms. + +* Added :add_month_numbers to select_month to get options like "3 - March". + +* Removed Base.has_active_layout? as it couldn't answer the question without the instance. Use Base#active_layout instead. + +* Removed redundant call to update on ActionController::Base#close_session [Andreas Schwarz] + +* Fixed that DRb Store accidently started its own server (instead of just client) [Andreas] + +* Fixed strip_links so it now works across multiple lines [Chad Fowler] + +* Fixed the TemplateError exception to show the proper trace on to_s (useful for unit test debugging) + +* Implemented class inheritable attributes without eval [Caio Chassot] + +* Made TextHelper#concat accept binding as it would otherwise not work + +* The FormOptionsHelper will now call to_s on the keys and values used to generate options + + +*0.8.5* + +* Introduced passing of locally scoped variables between templates: + + You can pass local variables to sub templates by using a hash of with the variable + names as keys and the objects as values: + + <%= render "shared/header", { "headline" => "Welcome", "person" => person } %> + + These can now be accessed in shared/header with: + + Headline: <%= headline %> + First name: <%= person.first_name %> + +* Introduced the concept of partials as a certain type of sub templates: + + There's also a convenience method for rendering sub templates within the current + controller that depends on a single object (we call this kind of sub templates for + partials). It relies on the fact that partials should follow the naming convention + of being prefixed with an underscore -- as to separate them from regular templates + that could be rendered on their own. In the template for Advertiser#buy, we could have: + + <% for ad in @advertisements %> + <%= render_partial "ad", ad %> + <% end %> + + This would render "advertiser/_ad.rhtml" and pass the local variable +ad+ + for the template to display. + + == Rendering a collection of partials + + The example of partial use describes a familar pattern where a template needs + to iterate over a array and render a sub template for each of the elements. + This pattern has been implemented as a single method that accepts an array and + renders a partial by the same name of as the elements contained within. So the + three-lined example in "Using partials" can be rewritten with a single line: + + <%= render_collection_of_partials "ad", @advertisements %> + + So this will render "advertiser/_ad.rhtml" and pass the local variable +ad+ for + the template to display. + +* Improved send_file by allowing a wide range of options to be applied [Jeremy Kemper]: + + Sends the file by streaming it 4096 bytes at a time. This way the + whole file doesn't need to be read into memory at once. This makes + it feasible to send even large files. + + Be careful to sanitize the path parameter if it coming from a web + page. send_file(@params['path'] allows a malicious user to + download any file on your server. + + Options: + * <tt>:filename</tt> - specifies the filename the browser will see. + Defaults to File.basename(path). + * <tt>:type</tt> - specifies an HTTP content type. + Defaults to 'application/octet-stream'. + * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. + Valid values are 'inline' and 'attachment' (default). + * <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used to stream + the file. Defaults to 4096. + + The default Content-Type and Content-Disposition headers are + set to download arbitrary binary files in as many browsers as + possible. IE versions 4, 5, 5.5, and 6 are all known to have + a variety of quirks (especially when downloading over SSL). + + Simple download: + send_file '/path/to.zip' + + Show a JPEG in browser: + send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline' + + Read about the other Content-* HTTP headers if you'd like to + provide the user with more information (such as Content-Description). + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11 + + Also be aware that the document may be cached by proxies and browsers. + The Pragma and Cache-Control headers declare how the file may be cached + by intermediaries. They default to require clients to validate with + the server before releasing cached responses. See + http://www.mnot.net/cache_docs/ for an overview of web caching and + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 + for the Cache-Control header spec. + +* Added pluralize method to the TextHelper that makes it easy to get strings like "1 message", "3 messages" + +* Added proper escaping for the rescues [Andreas Schwartz] + +* Added proper escaping for the option and collection tags [Andreas Schwartz] + +* Fixed NaN errors on benchmarking [Jim Weirich] + +* Fixed query string parsing for URLs that use the escaped versions of & or ; as part of a key or value + +* Fixed bug with custom Content-Type headers being in addition to rather than instead of the default header. + (This bug didn't matter with neither CGI or mod_ruby, but FCGI exploded on it) [With help from Ara T. Howard] + + +*0.8.0* + +* Added select, collection_select, and country_select to make it easier for Active Records to set attributes through + drop-down lists of options. Example: + + <%= select "person", "gender", %w( Male Female ) %> + + ...would give the following: + + <select name="person[gender]" id="person_gender"><option>Male</option><option>Female</option></select> + +* Added an option for getting multiple values on a single form name into an array instead of having the last one overwrite. + This is especially useful for groups of checkboxes, which can now be written as: + + <input type="checkbox" name="rights[]" value="CREATE" /> + <input type="checkbox" name="rights[]" value="UPDATE" /> + <input type="checkbox" name="rights[]" value="DELETE" /> + + ...and retrieved in the controller action with: + + @params["rights"] # => [ "CREATE", "UPDATE", "DELETE" ] + + The old behavior (where the last one wins, "DELETE" in the example) is still available. Just don't add "[]" to the + end of the name. [Scott Baron] + +* Added send_file which uses the new render_text block acceptance to make it feasible to send large files. + The files is sent with a bunch of voodoo HTTP headers required to get arbitrary files to download as + expected in as many browsers as possible (eg, IE hacks). Example: + + def play_movie + send_file "/movies/that_movie.avi" + end + + [Jeremy Kemper] + +* render_text now accepts a block for deferred rendering. Useful for streaming large files, displaying + a “please wait” message during a complex search, etc. Streaming example: + + render_text do |response| + File.open(path, 'rb') do |file| + while buf = file.read(1024) + print buf + end + end + end + + [Jeremy Kemper] + +* Added a new Tag Helper that can generate generic tags programmatically insted of through HTML. Example: + + tag("br", "clear" => "all") => <br clear="all" /> + + ...that's usually not terribly interesting (unless you have a lot of options already in a hash), but it + gives way for more specific tags, like the new form tag: + + form_tag({ :controller => "weblog", :action => "update" }, { :multipart => "true", "style" => "width: 200px"}) => + <form action="/weblog/update" enctype="multipart/formdata" style="width: 200px"> + + There's even a "pretty" version for people who don't like to open tags in code and close them in HTML: + + <%= start_form_tag :action => "update" %> + # all the input fields + <%= end_form_tag %> + + (end_form_tag just returns "</form>") + +* The selected parameter in options_for_select may now also an array of values to be selected when + using a multiple select. Example: + + options_for_select([ "VISA", "Mastercard", "Discover" ], ["VISA", "Discover"]) => + <option selected>VISA</option>\n<option>Mastercard</option>\n<option selected>Discover</option> + + [Scott Baron] + +* Changed the URL rewriter so controller_prefix and action_prefix can be used in isolation. You can now do: + + url_for(:controller_prefix => "clients") + + ...or: + + url_for(:action_prefix => "category/messages") + + Neither would have worked in isolation before (:controller_prefix required a :controller and :action_prefix required an :action) + +* Started process of a cleaner separation between Action Controller and ERb-based Action Views by introducing an + abstract base class for views. And Amita adapter could be fitted in more easily now. + +* The date helper methods date_select and datetime_select now also use the field error wrapping + (div with class fieldWithErrors by default). + +* The date helper methods date_select and datetime_select can now discard selects + +* Added option on AbstractTemplate to specify a different field error wrapping. Example: + + ActionView::AbstractTemplate.field_error_proc = Proc.new do |html, instance| + "<p>#{instance.method_name + instance.error_message}</p><div style='background-color: red'>#{html}</div>" + end + + ...would give the following on a Post#title (text field) error: + + <p>Title can't be empty</p> + <div style='background-color: red'> + <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /> + </div> + +* The UrlHelper methods url_for and link_to will now by default only return paths, not complete URIs. + That should make it easier to fit a Rails application behind a proxy or load-balancer. + You can overwrite this by passing :only_path => false as part of the options. [Suggested by U235] + +* Fixed bug with having your own layout for use with scaffolding [Kevin Radloff] + +* Fixed bug where redirect_to_path didn't append the port on non-standard ports [dhawkins] + +* Scaffolding plays nicely with single-table inheritance (LoadErrors are caught) [Jeremy Kemper] + +* Scaffolding plays nice with plural models like Category/categories [Jeremy Kemper] + +* Fixed missing suffix appending in scaffolding [Kevin Radloff] + + +*0.7.9* + +* The "form" method now present boolean fields from PostgreSQL as drop-down menu. [Scott] + +* Scaffolding now automatically attempts to require the class that's being scaffolded. + +* Scaffolding will use the current active layout, instead of its own, if one has been specified. Example: + + class WeblogController < ActionController::Base + layout "layouts/weblog" + scaffold :post + end + + [Suggested by Scott] + +* Changed url_for (and all the that drives, like redirect_to, link_to, link_for) so you can pass it a symbol instead of a hash. + This symbol is a method reference which is then called to calculate the url. Example: + + class WeblogController < ActionController::Base + def update + # do some update + redirect_to :dashboard_url + end + + protected + def dashboard_url + if @project.active? + url_for :controller => "project", :action => "dashboard" + else + url_for :controller => "account", :action => "dashboard" + end + end + end + +* Added default_url_options to specialize behavior for all url_for (and friends) calls: + + Overwrite to implement a number of default options that all url_for-based methods will use. + The default options should come in form of a hash, just like the one you would use for + url_for directly. Example: + + def default_url_options(options) + { :controller_prefix => @project.active? ? "projects/" : "accounts/" } + end + + As you can infer from the example, this is mostly useful for situations where you want to + centralize dynamic dissions about the urls as they stem from the business domain. Please note + that any individual url_for call can always override the defaults set by this method. + + +* Changed url_for so that an "id" passed in the :params is not treated special. You need to use the dedicated :id to get + the special auto path-params treatment. Considering the url http://localhost:81/friends/list + + url_for(:action => "show", :params => { "id" => 5 }) + ...used to give http://localhost:81/friends/show/5 + ......now gives http://localhost:81/friends/show?id=5 + + If you want the automated id behavior, do: + + url_for(:action => "show", :id => 5 ) + ....which gives http://localhost:81/friends/show/5 + + +* Fixed problem with anchor being inserted before path parameters with url_for (and friends) + + +*0.7.8* + +* Fixed session bug where you couldn't store any objects that didn't exist in the standard library + (such as Active Record objects). + +* Added reset_session method for Action Controller objects to clear out all objects in the session. + +* Fixed that exceptions raised during filters are now also caught by the default rescues + +* Added new around_filter for doing before and after filtering with a single object [Florian Weber]: + + class WeblogController < ActionController::Base + around_filter BenchmarkingFilter.new + + # Before this action is performed, BenchmarkingFilter#before(controller) is executed + def index + end + # After this action has been performed, BenchmarkingFilter#after(controller) is executed + end + + class BenchmarkingFilter + def initialize + @runtime + end + + def before + start_timer + end + + def after + stop_timer + report_result + end + end + +* Added the options for specifying a different name and id for the form helper methods than what is guessed [Florian Weber]: + + text_field "post", "title" + ...just gives: <input id="post_title" name="post[title]" size="30" type="text" value="" /> + + text_field "post", "title", "id" => "title_for_post", "name" => "first_post_title" + ...can now give: <input id="title_for_post" name="first_post_title" size="30" type="text" value="" /> + +* Added DebugHelper with a single "debug" method for doing pretty dumps of objects in the view + (now used in the default rescues to better present the contents of session and template variables) + +* Added note to log about the templates rendered within layouts (before just the layout was shown) + +* Fixed redirects on https setups [Andreas] + +* Fixed scaffolding problem on the edit action when using :suffix => true [Scott] + +* Fixed scaffolding problem where implementing list.rhtml wouldn't work for the index action + +* URLs generated now uses & instead of just & so pages using it can validate with W3C [Spotted by Andreas] + + +*0.7.7* + +* Fixed bug in CGI extension that prevented multipart forms from working + + +*0.7.6* + +* Included ERB::Util so all templates can easily escape HTML content with <%=h @person.content %> + +* All requests are now considered local by default, so everyone will be exposed to detailed debugging screens on errors. + When the application is ready to go public, set ActionController::Base.consider_all_requests_local to false, + and implement the protected method local_request? in the controller to determine when debugging screens should be shown. + +* Fixed three bugs with the url_for/redirect_to/link_to handling. Considering the url http://localhost:81/friends/show/1 + + url_for(:action => "list") + ...used to give http://localhost:81/friends/list/1 + ......now gives http://localhost:81/friends/list + + url_for(:controller => "friends", :action => "destroy", :id => 5) + ...used to give http://localhost:81/friends/destroy + ......now gives http://localhost:81/friends/destroy/5 + + Considering the url http://localhost:81/teachers/show/t + + url_for(:action => "list", :id => 5) + ...used to give http://localhost:81/5eachers/list/t + ......now gives http://localhost:81/teachers/list/5 + + [Reported by David Morton & Radsaq] + +* Logs exception to logfile in addition to showing them for local requests + +* Protects the eruby load behind a begin/rescue block. eRuby is not required to run ActionController. + +* Fixed install.rb to also install clean_logger and the templates + +* Added ActiveRecordStore as a session option. Read more in lib/action_controller/session/active_record_store.rb [Tim Bates] + +* Change license to MIT License (and included license file in package) + +* Application error page now returns status code 500 instead of 200 + +* Fixed using Procs as layout handlers [Florian Weber] + +* Fixed bug with using redirects ports other than 80 + +* Added index method that calls list on scaffolding + + +*0.7.5* + +* First public release
\ No newline at end of file diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE new file mode 100644 index 0000000000..26f55e7799 --- /dev/null +++ b/actionpack/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/actionpack/README b/actionpack/README new file mode 100755 index 0000000000..d22ca0a701 --- /dev/null +++ b/actionpack/README @@ -0,0 +1,418 @@ += Action Pack -- On rails from request to response + +Action Pack splits the response to a web request into a controller part +(performing the logic) and a view part (rendering a template). This two-step +approach is known as an action, which will normally create, read, update, or +delete (CRUD for short) some sort of model part (often backed by a database) +before choosing either to render a template or redirecting to another action. + +Action Pack implements these actions as public methods on Action Controllers +and uses Action Views to implement the template rendering. Action Controllers +are then responsible for handling all the actions relating to a certain part +of an application. This grouping usually consists of actions for lists and for +CRUDs revolving around a single (or a few) model objects. So ContactController +would be responsible for listing contacts, creating, deleting, and updating +contacts. A WeblogController could be responsible for both posts and comments. + +Action View templates are written using embedded Ruby in tags mingled in with +the HTML. To avoid cluttering the templates with code, a bunch of helper +classes provide common behavior for forms, dates, and strings. And it's easy +to add specific helpers to keep the separation as the application evolves. + +Note: Some of the features, such as scaffolding and form building, are tied to +ActiveRecord[http://activerecord.rubyonrails.org] (an object-relational +mapping package), but that doesn't mean that Action Pack depends on Active +Record. Action Pack is an independent package that can be used with any sort +of backend (Instiki[http://www.instiki.org], which is based on an older version +of Action Pack, uses Madeleine for example). Read more about the role Action +Pack can play when used together with Active Record on +http://www.rubyonrails.org. + +A short rundown of the major features: + +* Actions grouped in controller as methods instead of separate command objects + and can therefore helper share methods. + + BlogController < ActionController::Base + def display + @customer = find_customer + end + + def update + @customer = find_customer + @customer.attributes = @params["customer"] + @customer.save ? + redirect_to(:action => "display") : + render("customer/edit") + end + + private + def find_customer() Customer.find(@params["id"]) end + end + + Learn more in link:classes/ActionController/Base.html + + +* Embedded Ruby for templates (no new "easy" template language) + + <% for post in @posts %> + Title: <%= post.title %> + <% end %> + + All post titles: <%= @post.collect{ |p| p.title }.join ", " %> + + <% unless @person.is_client? %> + Not for clients to see... + <% end %> + + Learn more in link:classes/ActionView.html + + +* Builder-based templates (great for XML content, like RSS) + + xml.rss("version" => "2.0") do + xml.channel do + xml.title(@feed_title) + xml.link(@url) + xml.description "Basecamp: Recent items" + xml.language "en-us" + xml.ttl "40" + + for item in @recent_items + xml.item do + xml.title(item_title(item)) + xml.description(item_description(item)) + xml.pubDate(item_pubDate(item)) + xml.guid(@recent_items.url(item)) + xml.link(@recent_items.url(item)) + end + end + end + end + + +* Filters for pre and post processing of the response (as methods, procs, and classes) + + class WeblogController < ActionController::Base + before_filter :authenticate, :cache, :audit + after_filter { |c| c.response.body = GZip::compress(c.response.body) } + after_filter LocalizeFilter + + def list + # Before this action is run, the user will be authenticated, the cache + # will be examined to see if a valid copy of the results already + # exist, and the action will be logged for auditing. + + # After this action has run, the output will first be localized then + # compressed to minimize bandwith usage + end + + private + def authenticate + # Implement the filter will full access to both request and response + end + end + + Learn more in link:classes/ActionController/Filters/ClassMethods.html + + +* Helpers for forms, dates, action links, and text + + <%= text_field "post", "title", "size" => 30 %> + <%= html_date_select(Date.today) %> + <%= link_to "New post", :controller => "post", :action => "new" %> + <%= truncate(post.title, 25) %> + + Learn more in link:classes/ActionView/Helpers.html + + +* Layout sharing for template reuse (think simple version of Struts + Tiles[http://jakarta.apache.org/struts/userGuide/dev_tiles.html]) + + class WeblogController < ActionController::Base + layout "weblog_layout" + + def hello_world + end + end + + Layout file (called weblog_layout): + <html><body><%= @content_for_layout %></body></html> + + Template for hello_world action: + <h1>Hello world</h1> + + Result of running hello_world action: + <html><body><h1>Hello world</h1></body></html> + + Learn more in link:classes/ActionController/Layout.html + + +* Advanced redirection that makes pretty urls easy + + RewriteRule ^/library/books/([A-Z]+)([0-9]+)/([-_a-zA-Z0-9]+)$ \ + /books_controller.cgi?action=$3&type=$1&code=$2 [QSA] [L] + + Accessing /library/books/ISBN/0743536703/show calls BooksController#show + + From that URL, you can rewrite the redirect in a number of ways: + + redirect_to(:action => "edit") => + /library/books/ISBN/0743536703/edit + + redirect_to(:path_params => { "type" => "XTC", "code" => "12354345" }) => + /library/books/XTC/12354345/show + + redirect_to(:controller_prefix => "admin", :controller => "accounts") => + /admin/accounts/ + + Learn more in link:classes/ActionController/Base.html + + +* Easy testing of both controller and template result through TestRequest/Response + + class LoginControllerTest < Test::Unit::TestCase + def setup + @controller = LoginController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_failing_authenticate + process :authenticate, "user_name" => "nop", "password" => "" + assert_flash_has 'alert' + assert_redirected_to :action => "index" + end + end + + Learn more in link:classes/ActionController/TestRequest.html + + +* Automated benchmarking and integrated logging + + Processing WeblogController#index (for 127.0.0.1 at Fri May 28 00:41:55) + Parameters: {"action"=>"index", "controller"=>"weblog"} + Rendering weblog/index (200 OK) + Completed in 0.029281 (34 reqs/sec) + + If Active Record is used as the model, you'll have the database debugging + as well: + + Processing WeblogController#create (for 127.0.0.1 at Sat Jun 19 14:04:23) + Params: {"controller"=>"weblog", "action"=>"create", + "post"=>{"title"=>"this is good"} } + SQL (0.000627) INSERT INTO posts (title) VALUES('this is good') + Redirected to http://test/weblog/display/5 + Completed in 0.221764 (4 reqs/sec) | DB: 0.059920 (27%) + + You specify a logger through a class method, such as: + + ActionController::Base.logger = Logger.new("Application Log") + ActionController::Base.logger = Log4r::Logger.new("Application Log") + + +* Powerful debugging mechanism for local requests + + All exceptions raised on actions performed on the request of a local user + will be presented with a tailored debugging screen that includes exception + message, stack trace, request parameters, session contents, and the + half-finished response. + + Learn more in link:classes/ActionController/Rescue.html + + +* Scaffolding for Action Record model objects + + require 'account' # must be an Active Record class + class AccountController < ActionController::Base + scaffold :account + end + + The AccountController now has the full CRUD range of actions and default + templates: list, show, destroy, new, create, edit, update + + Learn more in link:classes/ActionController/Scaffolding/ClassMethods.html + + +* Form building for Active Record model objects + + The post object has a title (varchar), content (text), and + written_on (date) + + <%= form "post" %> + + ...will generate something like (the selects will have more options of + course): + + <form action="create" method="POST"> + <p> + <b>Title:</b><br/> + <input type="text" name="post[title]" value="<%= @post.title %>" /> + </p> + <p> + <b>Content:</b><br/> + <textarea name="post[content]"><%= @post.title %></textarea> + </p> + <p> + <b>Written on:</b><br/> + <select name='post[written_on(3i)]'><option>18</option></select> + <select name='post[written_on(2i)]'><option value='7'>July</option></select> + <select name='post[written_on(1i)]'><option>2004</option></select> + </p> + + <input type="submit" value="Create"> + </form> + + This form generates a @params["post"] array that can be used directly in a save action: + + class WeblogController < ActionController::Base + def save + post = Post.create(@params["post"]) + redirect_to :action => "display", :path_params => { "id" => post.id } + end + end + + Learn more in link:classes/ActionView/Helpers/ActiveRecordHelper.html + + +* Automated mapping of URLs to controller/action pairs through Apache's + mod_rewrite + + Requesting /blog/display/5 will call BlogController#display and + make 5 available as an instance variable through @params["id"] + + +* Runs on top of CGI, FCGI, and mod_ruby + + See the address_book_controller example for all three forms + + +== Simple example + +This example will implement a simple weblog system using inline templates and +an Active Record model. The first thing we need to do is setup an .htaccess to +interpret pretty URLs into something the controller can use. Let's use the +simplest form for starters: + + RewriteRule ^weblog/([-_a-zA-Z0-9]+)/([0-9]+)$ \ + /weblog_controller.cgi?action=$2&id=$3 [QSA] + RewriteRule ^weblog/([-_a-zA-Z0-9]+)$ \ + /weblog_controller.cgi?action=$2 [QSA] + RewriteRule ^weblog/$ \ + /weblog_controller.cgi?action=index [QSA] + +Now we'll be able to access URLs like weblog/display/5 and have +WeblogController#display called with { "id" => 5 } in the @params array +available for the action. So let's build that WeblogController with just a few +methods: + + require 'action_controller' + require 'post' + class WeblogController < ActionController::Base + layout "weblog/layout" + + def index + @posts = Post.find_all + end + + def display + @post = Post.find(@params["id"]) + end + + def new + @post = Post.new + end + + def create + @post = Post.create(@params["post"]) + @post.save + redirect_to :action => "display", :id => @post.id + end + end + + WeblogController::Base.template_root = File.dirname(__FILE__) + WeblogController.process_cgi if $0 == __FILE__ + +The last two lines are responsible for telling ActionController where the +template files are located and actually running the controller on a new +request from the web-server (like to be Apache). + +And the templates look like this: + + weblog/layout.rhtml: + <html><body> + <%= @content_for_layout %> + </body></html> + + weblog/index.rhtml: + <% for post in @posts %> + <p><%= link_to(post.title, :action => "display", :id => post.id %></p> + <% end %> + + weblog/display.rhtml: + <p> + <b><%= post.title %></b><br/> + <b><%= post.content %></b> + </p> + + weblog/new.rhtml: + <%= form "post" %> + +This simple setup will list all the posts in the system on the index page, +which is called by accessing /weblog/. It uses the form builder for the Active +Record model to make the new screen, which in turns hand everything over to +the create action (that's the default target for the form builder when given a +new model). After creating the post, it'll redirect to the display page using +an URL such as /weblog/display/5 (where 5 is the id of the post. + + +== Examples + +Action Pack ships with three examples that all demonstrate an increasingly +detailed view of the possibilities. First is blog_controller that is just a +single file for the whole MVC (but still split into separate parts). Second is +the debate_controller that uses separate template files and multiple screens. +Third is the address_book_controller that uses the layout feature to separate +template casing from content. + +Please note that you might need to change the "shebang" line to +#!/usr/local/env ruby, if your Ruby is not placed in /usr/local/bin/ruby + + +== Download + +The latest version of Action Pack can be found at + +* http://rubyforge.org/project/showfiles.php?group_id=249 + +Documentation can be found at + +* http://actionpack.rubyonrails.org + + +== Installation + +You can install Action Pack with the following command. + + % [sudo] ruby install.rb + +from its distribution directory. + + +== License + +Action Pack is released under the same license as Ruby. + + +== Support + +The Action Pack homepage is http://actionpack.rubyonrails.org. You can find +the Action Pack RubyForge page at http://rubyforge.org/projects/actionpack. +And as Jim from Rake says: + + Feel free to submit commits or feature requests. If you send a patch, + remember to update the corresponding unit tests. If fact, I prefer + new feature to be submitted in the form of new unit tests. + +For other information, feel free to ask on the ruby-talk mailing list (which +is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com.
\ No newline at end of file diff --git a/actionpack/RUNNING_UNIT_TESTS b/actionpack/RUNNING_UNIT_TESTS new file mode 100644 index 0000000000..c27ee02d67 --- /dev/null +++ b/actionpack/RUNNING_UNIT_TESTS @@ -0,0 +1,25 @@ +== Running with Rake + +The easiest way to run the unit tests is through Rake. The default task runs +the entire test suite for all classes. For more information, checkout the +full array of rake tasks with "rake -T" + +Rake can be found at http://rake.rubyforge.org + +== Running by hand + +If you only want to run a single test suite, or don't want to bother with Rake, +you can do so with something like: + + ruby controller/base_test.rb + +== Dependency on ActiveRecord and database setup + +Test cases in test/controller/active_record_assertions.rb depend on having +activerecord installed and configured in a particular way. See comment in the +test file itself for details. If ActiveRecord is not in +actionpack/../activerecord directory, these tests are skipped. If activerecord +is installed, but not configured as expected, the tests will fail. + +Other tests are runnable from a fresh copy of actionpack without any configuration. + diff --git a/actionpack/Rakefile b/actionpack/Rakefile new file mode 100755 index 0000000000..fb9f3c9bfb --- /dev/null +++ b/actionpack/Rakefile @@ -0,0 +1,105 @@ +require 'rubygems' +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/contrib/rubyforgepublisher' + +PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' +PKG_NAME = 'actionpack' +PKG_VERSION = '0.9.5' + PKG_BUILD +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" + +desc "Default Task" +task :default => [ :test ] + +# Run the unit tests + +Rake::TestTask.new { |t| + t.libs << "test" + t.pattern = 'test/*/*_test.rb' + t.verbose = true +} + + +# Genereate the RDoc documentation + +Rake::RDocTask.new { |rdoc| + rdoc.rdoc_dir = 'doc' + rdoc.title = "Action Pack -- On rails from request to response" + rdoc.options << '--line-numbers --inline-source --main README' + rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG') + rdoc.rdoc_files.include('lib/**/*.rb') +} + + +# Create compressed packages + + +dist_dirs = [ "lib", "test", "examples" ] + +spec = Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = PKG_NAME + s.version = PKG_VERSION + s.summary = "Web-flow and rendering framework putting the VC in MVC." + s.description = %q{Eases web-request routing, handling, and response as a half-way front, half-way page controller. Implemented with specific emphasis on enabling easy unit/integration testing that doesn't require a browser.} + + s.author = "David Heinemeier Hansson" + s.email = "david@loudthinking.com" + s.rubyforge_project = "actionpack" + s.homepage = "http://actionpack.rubyonrails.org" + + s.has_rdoc = true + s.requirements << 'none' + s.require_path = 'lib' + s.autorequire = 'action_controller' + + s.files = [ "rakefile", "install.rb", "README", "RUNNING_UNIT_TESTS", "CHANGELOG", "MIT-LICENSE", "examples/.htaccess" ] + dist_dirs.each do |dir| + s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "CVS" ) } + end + s.files.delete "examples/benchmark.rb" + s.files.delete "examples/benchmark_with_ar.fcgi" +end + +Rake::GemPackageTask.new(spec) do |p| + p.gem_spec = spec + p.need_tar = true + p.need_zip = true +end + + +# Publish beta gem +desc "Publish the API documentation" +task :pgem => [:package] do + Rake::SshFilePublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload + `ssh davidhh@one.textdrive.com './gemupdate.sh'` +end + +# Publish documentation +desc "Publish the API documentation" +task :pdoc => [:rdoc] do + Rake::SshDirPublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/ap", "doc").upload +end + + +desc "Count lines in the main rake file" +task :lines do + lines = 0 + codelines = 0 + Dir.foreach("lib/action_controller") { |file_name| + next unless file_name =~ /.*rb/ + + f = File.open("lib/action_controller/" + file_name) + + while line = f.gets + lines += 1 + next if line =~ /^\s*$/ + next if line =~ /^\s*#/ + codelines += 1 + end + } + puts "Lines #{lines}, LOC #{codelines}" +end
\ No newline at end of file diff --git a/actionpack/examples/.htaccess b/actionpack/examples/.htaccess new file mode 100644 index 0000000000..fb59fa105e --- /dev/null +++ b/actionpack/examples/.htaccess @@ -0,0 +1,24 @@ +<IfModule mod_ruby.c> + RubyRequire apache/ruby-run + RubySafeLevel 0 + + <Files *.rbx> + SetHandler ruby-object + RubyHandler Apache::RubyRun.instance + </Files> +</IfModule> + + +RewriteEngine On +RewriteRule ^fcgi/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ /$1_controller.fcgi?action=$2&id=$3 [QSA] +RewriteRule ^fcgi/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ /$1_controller.fcgi?action=$2 [QSA] +RewriteRule ^fcgi/([-_a-zA-Z0-9]+)/$ /$1_controller.fcgi?action=index [QSA] + +RewriteRule ^modruby/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ /$1_controller.rbx?action=$2&id=$3 [QSA] +RewriteRule ^modruby/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ /$1_controller.rbx?action=$2 [QSA] +RewriteRule ^modruby/([-_a-zA-Z0-9]+)/$ /$1_controller.rbx?action=index [QSA] + +RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ /$1_controller.cgi?action=$2&id=$3 [QSA] +RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ /$1_controller.cgi?action=$2 [QSA] +RewriteRule ^([-_a-zA-Z0-9]+)/$ /$1_controller.cgi?action=index [QSA] + diff --git a/actionpack/examples/address_book/index.rhtml b/actionpack/examples/address_book/index.rhtml new file mode 100644 index 0000000000..217d39075c --- /dev/null +++ b/actionpack/examples/address_book/index.rhtml @@ -0,0 +1,33 @@ +<h1>Address Book</h1> + +<% if @people.empty? %> + <p>No people in the address book yet</p> +<% else %> + <table> + <tr><th>Name</th><th>Email Address</th><th>Phone Number</th></tr> + <% for person in @people %> + <tr><td><%= person.name %></td><td><%= person.email_address %></td><td><%= person.phone_number %></td></tr> + <% end %> + </table> +<% end %> + +<form action="create_person"> + <p> + Name:<br /> + <input type="text" name="person[name]"> + </p> + + <p> + Email address:<br /> + <input type="text" name="person[email_address]"> + </p> + + <p> + Phone number:<br /> + <input type="text" name="person[phone_number]"> + </p> + + <p> + <input type="submit" value="Create Person"> + </p> +</form>
\ No newline at end of file diff --git a/actionpack/examples/address_book/layout.rhtml b/actionpack/examples/address_book/layout.rhtml new file mode 100644 index 0000000000..931e141c01 --- /dev/null +++ b/actionpack/examples/address_book/layout.rhtml @@ -0,0 +1,8 @@ +<html> +<head> + <title><%= @title || "Untitled" %></title> +</head> +<body> +<%= @content_for_layout %> +</body> +</html>
\ No newline at end of file diff --git a/actionpack/examples/address_book_controller.cgi b/actionpack/examples/address_book_controller.cgi new file mode 100755 index 0000000000..2e15467285 --- /dev/null +++ b/actionpack/examples/address_book_controller.cgi @@ -0,0 +1,9 @@ +#!/usr/local/bin/ruby + +require "address_book_controller" + +begin + AddressBookController.process_cgi(CGI.new) +rescue => e + CGI.new.out { "#{e.class}: #{e.message}" } +end
\ No newline at end of file diff --git a/actionpack/examples/address_book_controller.fcgi b/actionpack/examples/address_book_controller.fcgi new file mode 100755 index 0000000000..39947b4444 --- /dev/null +++ b/actionpack/examples/address_book_controller.fcgi @@ -0,0 +1,6 @@ +#!/usr/local/bin/ruby + +require "address_book_controller" +require "fcgi" + +FCGI.each_cgi { |cgi| AddressBookController.process_cgi(cgi) }
\ No newline at end of file diff --git a/actionpack/examples/address_book_controller.rb b/actionpack/examples/address_book_controller.rb new file mode 100644 index 0000000000..01d498e1bc --- /dev/null +++ b/actionpack/examples/address_book_controller.rb @@ -0,0 +1,52 @@ +$:.unshift(File.dirname(__FILE__) + "/../lib") + +require "action_controller" +require "action_controller/test_process" + +Person = Struct.new("Person", :id, :name, :email_address, :phone_number) + +class AddressBookService + attr_reader :people + + def initialize() @people = [] end + def create_person(data) people.unshift(Person.new(next_person_id, data["name"], data["email_address"], data["phone_number"])) end + def find_person(topic_id) people.select { |person| person.id == person.to_i }.first end + def next_person_id() people.first.id + 1 end +end + +class AddressBookController < ActionController::Base + layout "address_book/layout" + + before_filter :initialize_session_storage + + # Could also have used a proc + # before_filter proc { |c| c.instance_variable_set("@address_book", c.session["address_book"] ||= AddressBookService.new) } + + def index + @title = "Address Book" + @people = @address_book.people + end + + def person + @person = @address_book.find_person(@params["id"]) + end + + def create_person + @address_book.create_person(@params["person"]) + redirect_to :action => "index" + end + + private + def initialize_session_storage + @address_book = @session["address_book"] ||= AddressBookService.new + end +end + +ActionController::Base.template_root = File.dirname(__FILE__) +# ActionController::Base.logger = Logger.new("debug.log") # Remove first comment to turn on logging in current dir + +begin + AddressBookController.process_cgi(CGI.new) if $0 == __FILE__ +rescue => e + CGI.new.out { "#{e.class}: #{e.message}" } +end
\ No newline at end of file diff --git a/actionpack/examples/address_book_controller.rbx b/actionpack/examples/address_book_controller.rbx new file mode 100644 index 0000000000..8c04eeccc8 --- /dev/null +++ b/actionpack/examples/address_book_controller.rbx @@ -0,0 +1,4 @@ +#!/usr/local/bin/ruby + +require "address_book_controller" +AddressBookController.process_cgi(CGI.new)
\ No newline at end of file diff --git a/actionpack/examples/benchmark.rb b/actionpack/examples/benchmark.rb new file mode 100644 index 0000000000..1e10a0c962 --- /dev/null +++ b/actionpack/examples/benchmark.rb @@ -0,0 +1,52 @@ +$:.unshift(File.dirname(__FILE__) + "/../lib") + +require "action_controller" +require 'action_controller/test_process' + +Person = Struct.new("Person", :name, :address, :age) + +class BenchmarkController < ActionController::Base + def message + render_text "hello world" + end + + def list + @people = [ Person.new("David"), Person.new("Mary") ] + render_template "hello: <% for person in @people %>Name: <%= person.name %><% end %>" + end + + def form_helper + @person = Person.new "david", "hyacintvej", 24 + render_template( + "<% person = Person.new 'Mary', 'hyacintvej', 22 %> " + + "change the name <%= text_field 'person', 'name' %> and <%= text_field 'person', 'address' %> and <%= text_field 'person', 'age' %>" + ) + end +end + +#ActionController::Base.template_root = File.dirname(__FILE__) + +require "benchmark" + +RUNS = ARGV[0] ? ARGV[0].to_i : 50 + +require "profile" if ARGV[1] + +runtime = Benchmark.measure { + RUNS.times { BenchmarkController.process_test(ActionController::TestRequest.new({ "action" => "list" })) } +} + +puts "List: #{RUNS / runtime.real}" + + +runtime = Benchmark.measure { + RUNS.times { BenchmarkController.process_test(ActionController::TestRequest.new({ "action" => "message" })) } +} + +puts "Message: #{RUNS / runtime.real}" + +runtime = Benchmark.measure { + RUNS.times { BenchmarkController.process_test(ActionController::TestRequest.new({ "action" => "form_helper" })) } +} + +puts "Form helper: #{RUNS / runtime.real}" diff --git a/actionpack/examples/benchmark_with_ar.fcgi b/actionpack/examples/benchmark_with_ar.fcgi new file mode 100755 index 0000000000..b9de370e24 --- /dev/null +++ b/actionpack/examples/benchmark_with_ar.fcgi @@ -0,0 +1,89 @@ +#!/usr/local/bin/ruby + +begin + +$:.unshift(File.dirname(__FILE__) + "/../lib") +$:.unshift(File.dirname(__FILE__) + "/../../../edge/activerecord/lib") + +require 'fcgi' +require 'action_controller' +require 'action_controller/test_process' + +require 'active_record' + +class Post < ActiveRecord::Base; end + +ActiveRecord::Base.establish_connection(:adapter => "mysql", :database => "basecamp") + +SESSION_OPTIONS = { "database_manager" => CGI::Session::MemoryStore } + +class TestController < ActionController::Base + def index + render_template <<-EOT + <% for post in Post.find_all(nil,nil,100) %> + <%= post.title %> + <% end %> + EOT + end + + def show_one + render_template <<-EOT + <%= Post.find_first.title %> + EOT + end + + def text + render_text "hello world" + end + + def erb_text + render_template "hello <%= 'world' %>" + end + + def erb_loop + render_template <<-EOT + <% for post in 1..100 %> + <%= post %> + <% end %> + EOT + end + + def rescue_action(e) puts e.message + e.backtrace.join("\n") end +end + +if ARGV.empty? && ENV["REQUEST_URI"] + FCGI.each_cgi do |cgi| + TestController.process(ActionController::CgiRequest.new(cgi, SESSION_OPTIONS), ActionController::CgiResponse.new(cgi)).out + end +else + if ARGV.empty? + cgi = CGI.new + end + + require 'benchmark' + require 'profile' if ARGV[2] == "profile" + + RUNS = ARGV[1] ? ARGV[1].to_i : 50 + + runtime = Benchmark::measure { + RUNS.times { + if ARGV.empty? + TestController.process(ActionController::CgiRequest.new(cgi, SESSION_OPTIONS), ActionController::CgiResponse.new(cgi)) + else + response = TestController.process_test( + ActionController::TestRequest.new({"action" => ARGV[0]}) + ) + puts(response.body) if ARGV[2] == "show" + end + } + } + + puts "Runs: #{RUNS}" + puts "Avg. runtime: #{runtime.real / RUNS}" + puts "Requests/second: #{RUNS / runtime.real}" +end + +rescue Exception => e + # CGI.new.out { "<pre>" + e.message + e.backtrace.join("\n") + "</pre>" } + $stderr << e.message + e.backtrace.join("\n") +end
\ No newline at end of file diff --git a/actionpack/examples/blog_controller.cgi b/actionpack/examples/blog_controller.cgi new file mode 100755 index 0000000000..e64fe85f0c --- /dev/null +++ b/actionpack/examples/blog_controller.cgi @@ -0,0 +1,53 @@ +#!/usr/local/bin/ruby + +$:.unshift(File.dirname(__FILE__) + "/../lib") + +require "action_controller" + +Post = Struct.new("Post", :title, :body) + +class BlogController < ActionController::Base + before_filter :initialize_session_storage + + def index + @posts = @session["posts"] + + render_template <<-"EOF" + <html><body> + <%= @flash["alert"] %> + <h1>Posts</h1> + <% @posts.each do |post| %> + <p><b><%= post.title %></b><br /><%= post.body %></p> + <% end %> + + <h1>Create post</h1> + <form action="create"> + Title: <input type="text" name="post[title]"><br> + Body: <textarea name="post[body]"></textarea><br> + <input type="submit" value="save"> + </form> + + </body></html> + EOF + end + + def create + @session["posts"].unshift(Post.new(@params["post"]["title"], @params["post"]["body"])) + flash["alert"] = "New post added!" + redirect_to :action => "index" + end + + private + def initialize_session_storage + @session["posts"] = [] if @session["posts"].nil? + end +end + +ActionController::Base.template_root = File.dirname(__FILE__) +# ActionController::Base.logger = Logger.new("debug.log") # Remove first comment to turn on logging in current dir + +begin + BlogController.process_cgi(CGI.new) if $0 == __FILE__ +rescue => e + CGI.new.out { "#{e.class}: #{e.message}" } +end
\ No newline at end of file diff --git a/actionpack/examples/debate/index.rhtml b/actionpack/examples/debate/index.rhtml new file mode 100644 index 0000000000..ddaa87da57 --- /dev/null +++ b/actionpack/examples/debate/index.rhtml @@ -0,0 +1,14 @@ +<html> +<body> +<h1>Topics</h1> + +<%= link_to "New topic", :action => "new_topic" %> + +<ul> +<% for topic in @topics %> + <li><%= link_to "#{topic.title} (#{topic.replies.length} replies)", :action => "topic", :path_params => { "id" => topic.id } %></li> +<% end %> +</ul> + +</body> +</html>
\ No newline at end of file diff --git a/actionpack/examples/debate/new_topic.rhtml b/actionpack/examples/debate/new_topic.rhtml new file mode 100644 index 0000000000..f52a69cc31 --- /dev/null +++ b/actionpack/examples/debate/new_topic.rhtml @@ -0,0 +1,22 @@ +<html> +<body> +<h1>New topic</h1> + +<form action="<%= url_for(:action => "create_topic") %>" method="post"> + <p> + Title:<br> + <input type="text" name="topic[title]"> + </p> + + <p> + Body:<br> + <textarea name="topic[body]" style="width: 200px; height: 200px"></textarea> + </p> + + <p> + <input type="submit" value="Create topic"> + </p> +</form> + +</body> +</html>
\ No newline at end of file diff --git a/actionpack/examples/debate/topic.rhtml b/actionpack/examples/debate/topic.rhtml new file mode 100644 index 0000000000..e247c00f0d --- /dev/null +++ b/actionpack/examples/debate/topic.rhtml @@ -0,0 +1,32 @@ +<html> +<body> +<h1><%= @topic.title %></h1> + +<p><%= @topic.body %></p> + +<%= link_to "Back to topics", :action => "index" %> + +<% unless @topic.replies.empty? %> + <h2>Replies</h2> + <ol> + <% for reply in @topic.replies %> + <li><%= reply.body %></li> + <% end %> + </ol> +<% end %> + +<h2>Reply to this topic</h2> + +<form action="<%= url_for(:action => "create_reply") %>" method="post"> + <input type="hidden" name="reply[topic_id]" value="<%= @topic.id %>"> + <p> + <textarea name="reply[body]" style="width: 200px; height: 200px"></textarea> + </p> + + <p> + <input type="submit" value="Create reply"> + </p> +</form> + +</body> +</html>
\ No newline at end of file diff --git a/actionpack/examples/debate_controller.cgi b/actionpack/examples/debate_controller.cgi new file mode 100755 index 0000000000..b82ac6259d --- /dev/null +++ b/actionpack/examples/debate_controller.cgi @@ -0,0 +1,57 @@ +#!/usr/local/bin/ruby + +$:.unshift(File.dirname(__FILE__) + "/../lib") + +require "action_controller" + +Topic = Struct.new("Topic", :id, :title, :body, :replies) +Reply = Struct.new("Reply", :body) + +class DebateService + attr_reader :topics + + def initialize() @topics = [] end + def create_topic(data) topics.unshift(Topic.new(next_topic_id, data["title"], data["body"], [])) end + def create_reply(data) find_topic(data["topic_id"]).replies << Reply.new(data["body"]) end + def find_topic(topic_id) topics.select { |topic| topic.id == topic_id.to_i }.first end + def next_topic_id() topics.first.id + 1 end +end + +class DebateController < ActionController::Base + before_filter :initialize_session_storage + + def index + @topics = @debate.topics + end + + def topic + @topic = @debate.find_topic(@params["id"]) + end + + # def new_topic() end <-- This is not needed as the template doesn't require any assigns + + def create_topic + @debate.create_topic(@params["topic"]) + redirect_to :action => "index" + end + + def create_reply + @debate.create_reply(@params["reply"]) + redirect_to :action => "topic", :path_params => { "id" => @params["reply"]["topic_id"] } + end + + private + def initialize_session_storage + @session["debate"] = DebateService.new if @session["debate"].nil? + @debate = @session["debate"] + end +end + +ActionController::Base.template_root = File.dirname(__FILE__) +# ActionController::Base.logger = Logger.new("debug.log") # Remove first comment to turn on logging in current dir + +begin + DebateController.process_cgi(CGI.new) if $0 == __FILE__ +rescue => e + CGI.new.out { "#{e.class}: #{e.message}" } +end
\ No newline at end of file diff --git a/actionpack/install.rb b/actionpack/install.rb new file mode 100644 index 0000000000..758c476a70 --- /dev/null +++ b/actionpack/install.rb @@ -0,0 +1,97 @@ +require 'rbconfig' +require 'find' +require 'ftools' + +include Config + +# this was adapted from rdoc's install.rb by ways of Log4r + +$sitedir = CONFIG["sitelibdir"] +unless $sitedir + version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"] + $libdir = File.join(CONFIG["libdir"], "ruby", version) + $sitedir = $:.find {|x| x =~ /site_ruby/ } + if !$sitedir + $sitedir = File.join($libdir, "site_ruby") + elsif $sitedir !~ Regexp.quote(version) + $sitedir = File.join($sitedir, version) + end +end + +makedirs = %w{ action_controller/assertions action_controller/cgi_ext + action_controller/session action_controller/support + action_controller/templates action_controller/templates/rescues + action_controller/templates/scaffolds + action_view/helpers action_view/vendor action_view/vendor/builder +} + + +makedirs.each {|f| File::makedirs(File.join($sitedir, *f.split(/\//)))} + +# deprecated files that should be removed +# deprecated = %w{ } + +# files to install in library path +files = %w- + action_controller.rb + action_controller/assertions/action_pack_assertions.rb + action_controller/assertions/active_record_assertions.rb + action_controller/base.rb + action_controller/benchmarking.rb + action_controller/cgi_ext/cgi_ext.rb + action_controller/cgi_ext/cgi_methods.rb + action_controller/cgi_process.rb + action_controller/filters.rb + action_controller/flash.rb + action_controller/helpers.rb + action_controller/layout.rb + action_controller/request.rb + action_controller/rescue.rb + action_controller/response.rb + action_controller/scaffolding.rb + action_controller/session/active_record_store.rb + action_controller/session/drb_server.rb + action_controller/session/drb_store.rb + action_controller/support/class_inheritable_attributes.rb + action_controller/support/class_attribute_accessors.rb + action_controller/support/clean_logger.rb + action_controller/support/cookie_performance_fix.rb + action_controller/support/inflector.rb + action_controller/templates/rescues/_request_and_response.rhtml + action_controller/templates/rescues/diagnostics.rhtml + action_controller/templates/rescues/layout.rhtml + action_controller/templates/rescues/missing_template.rhtml + action_controller/templates/rescues/template_error.rhtml + action_controller/templates/rescues/unknown_action.rhtml + action_controller/templates/scaffolds/edit.rhtml + action_controller/templates/scaffolds/layout.rhtml + action_controller/templates/scaffolds/list.rhtml + action_controller/templates/scaffolds/new.rhtml + action_controller/templates/scaffolds/show.rhtml + action_controller/test_process.rb + action_controller/url_rewriter.rb + action_view.rb + action_view/base.rb + action_view/helpers/active_record_helper.rb + action_view/helpers/date_helper.rb + action_view/helpers/debug_helper.rb + action_view/helpers/form_helper.rb + action_view/helpers/form_options_helper.rb + action_view/helpers/text_helper.rb + action_view/helpers/tag_helper.rb + action_view/helpers/url_helper.rb + action_view/partials.rb + action_view/template_error.rb + action_view/vendor/builder.rb + action_view/vendor/builder/blankslate.rb + action_view/vendor/builder/xmlbase.rb + action_view/vendor/builder/xmlevents.rb + action_view/vendor/builder/xmlmarkup.rb +- + +# the acual gruntwork +Dir.chdir("lib") +# File::safe_unlink *deprecated.collect{|f| File.join($sitedir, f.split(/\//))} +files.each {|f| + File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true) +} diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb new file mode 100755 index 0000000000..6445940d78 --- /dev/null +++ b/actionpack/lib/action_controller.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. +#++ + +$:.unshift(File.dirname(__FILE__)) + +require 'action_controller/support/clean_logger' + +require 'action_controller/base' +require 'action_controller/rescue' +require 'action_controller/benchmarking' +require 'action_controller/filters' +require 'action_controller/layout' +require 'action_controller/flash' +require 'action_controller/scaffolding' +require 'action_controller/helpers' +require 'action_controller/dependencies' +require 'action_controller/cgi_process' + +ActionController::Base.class_eval do + include ActionController::Filters + include ActionController::Layout + include ActionController::Flash + include ActionController::Benchmarking + include ActionController::Rescue + include ActionController::Scaffolding + include ActionController::Helpers + include ActionController::Dependencies +end + +require 'action_view' +ActionController::Base.template_class = ActionView::Base
\ No newline at end of file diff --git a/actionpack/lib/action_controller/assertions/action_pack_assertions.rb b/actionpack/lib/action_controller/assertions/action_pack_assertions.rb new file mode 100644 index 0000000000..2cfbcbc938 --- /dev/null +++ b/actionpack/lib/action_controller/assertions/action_pack_assertions.rb @@ -0,0 +1,199 @@ +require 'test/unit' +require 'test/unit/assertions' +require 'rexml/document' + +module Test #:nodoc: + module Unit #:nodoc: + # Adds a wealth of assertions to do functional testing of Action Controllers. + module Assertions + # -- basic assertions --------------------------------------------------- + + # ensure that the web request has been serviced correctly + def assert_success(message=nil) + response = acquire_assertion_target + if response.success? + # to count the assertion + assert_block("") { true } + else + if response.redirect? + msg = build_message(message, "Response unexpectedly redirect to <?>", response.redirect_url) + else + msg = build_message(message, "unsuccessful request (response code = <?>)", + response.response_code) + end + assert_block(msg) { false } + end + end + + # ensure the request was rendered with the appropriate template file + def assert_rendered_file(expected=nil, message=nil) + response = acquire_assertion_target + rendered = expected ? response.rendered_file(!expected.include?('/')) : response.rendered_file + msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered) + assert_block(msg) do + if expected.nil? + response.rendered_with_file? + else + expected == rendered + end + end + end + + # -- session assertions ------------------------------------------------- + + # ensure that the session has an object with the specified name + def assert_session_has(key=nil, message=nil) + response = acquire_assertion_target + msg = build_message(message, "<?> is not in the session <?>", key, response.session) + assert_block(msg) { response.has_session_object?(key) } + end + + # ensure that the session has no object with the specified name + def assert_session_has_no(key=nil, message=nil) + response = acquire_assertion_target + msg = build_message(message, "<?> is in the session <?>", key, response.session) + assert_block(msg) { !response.has_session_object?(key) } + end + + def assert_session_equal(expected = nil, key = nil, message = nil) + response = acquire_assertion_target + msg = build_message(message, "<?> expected in session['?'] but was <?>", expected, key, response.session[key]) + assert_block(msg) { expected == response.session[key] } + end + + # -- flash assertions --------------------------------------------------- + + # ensure that the flash has an object with the specified name + def assert_flash_has(key=nil, message=nil) + response = acquire_assertion_target + msg = build_message(message, "<?> is not in the flash <?>", key, response.flash) + assert_block(msg) { response.has_flash_object?(key) } + end + + # ensure that the flash has no object with the specified name + def assert_flash_has_no(key=nil, message=nil) + response = acquire_assertion_target + msg = build_message(message, "<?> is in the flash <?>", key, response.flash) + assert_block(msg) { !response.has_flash_object?(key) } + end + + # ensure the flash exists + def assert_flash_exists(message=nil) + response = acquire_assertion_target + msg = build_message(message, "the flash does not exist <?>", response.session['flash'] ) + assert_block(msg) { response.has_flash? } + end + + # ensure the flash does not exist + def assert_flash_not_exists(message=nil) + response = acquire_assertion_target + msg = build_message(message, "the flash exists <?>", response.flash) + assert_block(msg) { !response.has_flash? } + end + + # ensure the flash is empty but existant + def assert_flash_empty(message=nil) + response = acquire_assertion_target + msg = build_message(message, "the flash is not empty <?>", response.flash) + assert_block(msg) { !response.has_flash_with_contents? } + end + + # ensure the flash is not empty + def assert_flash_not_empty(message=nil) + response = acquire_assertion_target + msg = build_message(message, "the flash is empty") + assert_block(msg) { response.has_flash_with_contents? } + end + + def assert_flash_equal(expected = nil, key = nil, message = nil) + response = acquire_assertion_target + msg = build_message(message, "<?> expected in flash['?'] but was <?>", expected, key, response.flash[key]) + assert_block(msg) { expected == response.flash[key] } + end + + # -- redirection assertions --------------------------------------------- + + # ensure we have be redirected + def assert_redirect(message=nil) + response = acquire_assertion_target + msg = build_message(message, "response is not a redirection (response code is <?>)", response.response_code) + assert_block(msg) { response.redirect? } + end + + def assert_redirected_to(options = {}, message=nil) + assert_redirect(message) + response = acquire_assertion_target + + msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is <?>)", response.redirected_to) + assert_block(msg) do + if options.is_a?(Symbol) + response.redirected_to == options + else + options.keys.all? { |k| options[k] == response.redirected_to[k] } + end + end + end + + # ensure our redirection url is an exact match + def assert_redirect_url(url=nil, message=nil) + assert_redirect(message) + response = acquire_assertion_target + msg = build_message(message, "<?> is not the redirected location <?>", url, response.redirect_url) + assert_block(msg) { response.redirect_url == url } + end + + # ensure our redirection url matches a pattern + def assert_redirect_url_match(pattern=nil, message=nil) + assert_redirect(message) + response = acquire_assertion_target + msg = build_message(message, "<?> was not found in the location: <?>", pattern, response.redirect_url) + assert_block(msg) { response.redirect_url_match?(pattern) } + end + + # -- template assertions ------------------------------------------------ + + # ensure that a template object with the given name exists + def assert_template_has(key=nil, message=nil) + response = acquire_assertion_target + msg = build_message(message, "<?> is not a template object", key ) + assert_block(msg) { response.has_template_object?(key) } + end + + # ensure that a template object with the given name does not exist + def assert_template_has_no(key=nil,message=nil) + response = acquire_assertion_target + msg = build_message(message, "<?> is a template object <?>", key, response.template_objects[key]) + assert_block(msg) { !response.has_template_object?(key) } + end + + # ensures that the object assigned to the template on +key+ is equal to +expected+ object. + def assert_assigned_equal(expected = nil, key = nil, message = nil) + response = acquire_assertion_target + msg = build_message(message, "<?> expected in assigns['?'] but was <?>", expected, key, response.template.assigns[key.to_s]) + assert_block(msg) { expected == response.template.assigns[key.to_s] } + end + + # Asserts that the template returns the +expected+ string or array based on the XPath +expression+. + # This will only work if the template rendered a valid XML document. + def assert_template_xpath_match(expression=nil, expected=nil, message=nil) + response = acquire_assertion_target + xml, matches = REXML::Document.new(response.body), [] + xml.elements.each(expression) { |e| matches << e.text } + matches = matches.first if matches.length < 2 + + msg = build_message(message, "<?> found <?>, not <?>", expression, matches, expected) + assert_block(msg) { matches == expected } + end + + # -- helper functions --------------------------------------------------- + + # get the TestResponse object that these assertions depend upon + def acquire_assertion_target + target = ActionController::TestResponse.assertion_target + assert_block( "Unable to acquire the TestResponse.assertion_target. Please set this before calling this assertion." ) { !target.nil? } + target + end + + end # Assertions + end # Unit +end # Test diff --git a/actionpack/lib/action_controller/assertions/active_record_assertions.rb b/actionpack/lib/action_controller/assertions/active_record_assertions.rb new file mode 100644 index 0000000000..9167eae53e --- /dev/null +++ b/actionpack/lib/action_controller/assertions/active_record_assertions.rb @@ -0,0 +1,65 @@ +require 'test/unit' +require 'test/unit/assertions' +# active_record is assumed to be loaded by this point + +module Test #:nodoc: + module Unit #:nodoc: + module Assertions + # Assert the template object with the given name is an Active Record descendant and is valid. + def assert_valid_record(key = nil, message = nil) + record = find_record_in_template(key) + msg = build_message(message, "Active Record is invalid <?>)", record.errors.full_messages) + assert_block(msg) { record.valid? } + end + + # Assert the template object with the given name is an Active Record descendant and is invalid. + def assert_invalid_record(key = nil, message = nil) + record = find_record_in_template(key) + msg = build_message(message, "Active Record is valid)") + assert_block(msg) { !record.valid? } + end + + # Assert the template object with the given name is an Active Record descendant and the specified column(s) are valid. + def assert_valid_column_on_record(key = nil, columns = "", message = nil) + record = find_record_in_template(key) + record.validate + + cols = glue_columns(columns) + cols.delete_if { |col| !record.errors.invalid?(col) } + msg = build_message(message, "Active Record has invalid columns <?>)", cols.join(",") ) + assert_block(msg) { cols.empty? } + end + + # Assert the template object with the given name is an Active Record descendant and the specified column(s) are invalid. + def assert_invalid_column_on_record(key = nil, columns = "", message = nil) + record = find_record_in_template(key) + record.validate + + cols = glue_columns(columns) + cols.delete_if { |col| record.errors.invalid?(col) } + msg = build_message(message, "Active Record has valid columns <?>)", cols.join(",") ) + assert_block(msg) { cols.empty? } + end + + private + def glue_columns(columns) + cols = [] + cols << columns if columns.class == String + cols += columns if columns.class == Array + cols + end + + def find_record_in_template(key = nil) + response = acquire_assertion_target + + assert_template_has(key) + record = response.template_objects[key] + + assert_not_nil(record) + assert_kind_of ActiveRecord::Base, record + + return record + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb new file mode 100755 index 0000000000..0cbcbbf688 --- /dev/null +++ b/actionpack/lib/action_controller/base.rb @@ -0,0 +1,689 @@ +require 'action_controller/request' +require 'action_controller/response' +require 'action_controller/url_rewriter' +require 'action_controller/support/class_attribute_accessors' +require 'action_controller/support/class_inheritable_attributes' +require 'action_controller/support/inflector' + +module ActionController #:nodoc: + class ActionControllerError < StandardError #:nodoc: + end + class SessionRestoreError < ActionControllerError #:nodoc: + end + class MissingTemplate < ActionControllerError #:nodoc: + end + class UnknownAction < ActionControllerError #:nodoc: + end + + # Action Controllers are made up of one or more actions that performs its purpose and then either renders a template or + # redirects to another action. An action is defined as a public method on the controller, which will automatically be + # made accessible to the web-server through a mod_rewrite mapping. A sample controller could look like this: + # + # class GuestBookController < ActionController::Base + # def index + # @entries = Entry.find_all + # end + # + # def sign + # Entry.create(@params["entry"]) + # redirect_to :action => "index" + # end + # end + # + # GuestBookController.template_root = "templates/" + # GuestBookController.process_cgi + # + # All actions assume that you want to render a template matching the name of the action at the end of the performance + # unless you tell it otherwise. The index action complies with this assumption, so after populating the @entries instance + # variable, the GuestBookController will render "templates/guestbook/index.rhtml". + # + # Unlike index, the sign action isn't interested in rendering a template. So after performing its main purpose (creating a + # new entry in the guest book), it sheds the rendering assumption and initiates a redirect instead. This redirect works by + # returning an external "302 Moved" HTTP response that takes the user to the index action. + # + # The index and sign represent the two basic action archetypes used in Action Controllers. Get-and-show and do-and-redirect. + # Most actions are variations of these themes. + # + # Also note that it's the final call to <tt>process_cgi</tt> that actually initiates the action performance. It will extract + # request and response objects from the CGI + # + # == Requests + # + # Requests are processed by the Action Controller framework by extracting the value of the "action" key in the request parameters. + # This value should hold the name of the action to be performed. Once the action has been identified, the remaining + # request parameters, the session (if one is available), and the full request with all the http headers are made available to + # the action through instance variables. Then the action is performed. + # + # The full request object is available in @request and is primarily used to query for http headers. These queries are made by + # accessing the environment hash, like this: + # + # def hello_ip + # location = @request.env["REMOTE_ADDRESS"] + # render_text "Hello stranger from #{location}" + # end + # + # == Parameters + # + # All request parameters whether they come from a GET or POST request, or from the URL, are available through the @params hash. + # So an action that was performed through /weblog/list?category=All&limit=5 will include { "category" => "All", "limit" => 5 } + # in @params. + # + # It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as: + # + # <input type="text" name="post[name]" value="david"> + # <input type="text" name="post[address]" value="hyacintvej"> + # + # A request stemming from a form holding these inputs will include { "post" # => { "name" => "david", "address" => "hyacintvej" } }. + # If the address input had been named "post[address][street]", the @params would have included + # { "post" => { "address" => { "street" => "hyacintvej" } } }. There's no limit to the depth of the nesting. + # + # == Sessions + # + # Sessions allows you to store objects in memory between requests. This is useful for objects that are not yet ready to be persisted, + # such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such + # as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely + # they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at. + # + # You can place objects in the session by using the <tt>@session</tt> hash: + # + # @session["person"] = Person.authenticate(user_name, password) + # + # And retrieved again through the same hash: + # + # Hello #{@session["person"]} + # + # Any object can be placed in the session (as long as it can be Marshalled). But remember that 1000 active sessions each storing a + # 50kb object could lead to a 50MB memory overhead. In other words, think carefully about size and caching before resorting to the use + # of the session. + # + # == Responses + # + # Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response + # object is generated automatically through the use of renders and redirects, so it's normally nothing you'll need to be concerned about. + # + # == Renders + # + # Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering + # of a template. Included in the Action Pack is the Action View, which enables rendering of ERb templates. It's automatically configured. + # The controller passes objects to the view by assigning instance variables: + # + # def show + # @post = Post.find(@params["id"]) + # end + # + # Which are then automatically available to the view: + # + # Title: <%= @post.title %> + # + # You don't have to rely on the automated rendering. Especially actions that could result in the rendering of different templates will use + # the manual rendering methods: + # + # def search + # @results = Search.find(@params["query"]) + # case @results + # when 0 then render "weblog/no_results" + # when 1 then render_action "show" + # when 2..10 then render_action "show_many" + # end + # end + # + # Read more about writing ERb and Builder templates in link:classes/ActionView/Base.html. + # + # == Redirects + # + # Redirecting is what actions that update the model do when they're done. The <tt>save_post</tt> method shouldn't be responsible for also + # showing the post once it's saved -- that's the job for <tt>show_post</tt>. So once <tt>save_post</tt> has completed its business, it'll + # redirect to <tt>show_post</tt>. All redirects are external, which means that when the user refreshes his browser, it's not going to save + # the post again, but rather just show it one more time. + # + # This sounds fairly simple, but the redirection is complicated by the quest for a phenomenon known as "pretty urls". Instead of accepting + # the dreadful beings that is "weblog_controller?action=show&post_id=5", Action Controller goes out of its way to represent the former as + # "/weblog/show/5". And this is even the simple case. As an example of a more advanced pretty url consider + # "/library/books/ISBN/0743536703/show", which can be mapped to books_controller?action=show&type=ISBN&id=0743536703. + # + # Redirects work by rewriting the URL of the current action. So if the show action was called by "/library/books/ISBN/0743536703/show", + # we can redirect to an edit action simply by doing <tt>redirect_to(:action => "edit")</tt>, which could throw the user to + # "/library/books/ISBN/0743536703/edit". Naturally, you'll need to setup the .htaccess (or other means of URL rewriting for the web server) + # to point to the proper controller and action in the first place, but once you have, it can be rewritten with ease. + # + # Let's consider a bunch of examples on how to go from "/library/books/ISBN/0743536703/edit" to somewhere else: + # + # redirect_to(:action => "show", :action_prefix => "XTC/123") => + # "http://www.singlefile.com/library/books/XTC/123/show" + # + # redirect_to(:path_params => {"type" => "EXBC"}) => + # "http://www.singlefile.com/library/books/EXBC/0743536703/show" + # + # redirect_to(:controller => "settings") => + # "http://www.singlefile.com/library/settings/" + # + # For more examples of redirecting options, have a look at the unit test in test/controller/url_test.rb. It's very readable and will give + # you an excellent understanding of the different options and what they do. + # + # == Environments + # + # Action Controller works out of the box with CGI, FastCGI, and mod_ruby. CGI and mod_ruby controllers are triggered just the same using: + # + # WeblogController.process_cgi + # + # FastCGI controllers are triggered using: + # + # FCGI.each_cgi{ |cgi| WeblogController.process_cgi(cgi) } + class Base + include ClassInheritableAttributes + + DEFAULT_RENDER_STATUS_CODE = "200 OK" + + DEFAULT_SEND_FILE_OPTIONS = { + :type => 'application/octet_stream', + :disposition => 'attachment', + :stream => true, + :buffer_size => 4096 + } + + + # Determines whether the view has access to controller internals @request, @response, @session, and @template. + # By default, it does. + @@view_controller_internals = true + cattr_accessor :view_controller_internals + + # All requests are considered local by default, so everyone will be exposed to detailed debugging screens on errors. + # When the application is ready to go public, this should be set to false, and the protected method <tt>local_request?</tt> + # should instead be implemented in the controller to determine when debugging screens should be shown. + @@consider_all_requests_local = true + cattr_accessor :consider_all_requests_local + + # When turned on (which is default), all dependencies are included using "load". This mean that any change is instant in cached + # environments like mod_ruby or FastCGI. When set to false, "require" is used, which is faster but requires server restart to + # be effective. + @@reload_dependencies = true + cattr_accessor :reload_dependencies + + # Template root determines the base from which template references will be made. So a call to render("test/template") + # will be converted to "#{template_root}/test/template.rhtml". + cattr_accessor :template_root + + # The logger is used for generating information on the action run-time (including benchmarking) if available. + # Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers. + cattr_accessor :logger + + # Determines which template class should be used by ActionController. + cattr_accessor :template_class + + # Turn on +ignore_missing_templates+ if you want to unit test actions without making the associated templates. + cattr_accessor :ignore_missing_templates + + # Holds the request object that's primarily used to get environment variables through access like + # <tt>@request.env["REQUEST_URI"]</tt>. + attr_accessor :request + + # Holds a hash of all the GET, POST, and Url parameters passed to the action. Accessed like <tt>@params["post_id"]</tt> + # to get the post_id. No type casts are made, so all values are returned as strings. + attr_accessor :params + + # Holds the response object that's primarily used to set additional HTTP headers through access like + # <tt>@response.headers["Cache-Control"] = "no-cache"</tt>. Can also be used to access the final body HTML after a template + # has been rendered through @response.body -- useful for <tt>after_filter</tt>s that wants to manipulate the output, + # such as a OutputCompressionFilter. + attr_accessor :response + + # Holds a hash of objects in the session. Accessed like <tt>@session["person"]</tt> to get the object tied to the "person" + # key. The session will hold any type of object as values, but the key should be a string. + attr_accessor :session + + # Holds a hash of header names and values. Accessed like <tt>@headers["Cache-Control"]</tt> to get the value of the Cache-Control + # directive. Values should always be specified as strings. + attr_accessor :headers + + # Holds a hash of cookie names and values. Accessed like <tt>@cookies["user_name"]</tt> to get the value of the user_name cookie. + # This hash is read-only. You set new cookies using the cookie method. + attr_accessor :cookies + + # Holds the hash of variables that are passed on to the template class to be made available to the view. This hash + # is generated by taking a snapshot of all the instance variables in the current scope just before a template is rendered. + attr_accessor :assigns + + class << self + # Factory for the standard create, process loop where the controller is discarded after processing. + def process(request, response) #:nodoc: + new.process(request, response) + end + + # Converts the class name from something like "OneModule::TwoModule::NeatController" to "NeatController". + def controller_class_name + Inflector.demodulize(name) + end + + # Converts the class name from something like "OneModule::TwoModule::NeatController" to "neat". + def controller_name + Inflector.underscore(controller_class_name.sub(/Controller/, "")) + end + + # Loads the <tt>file_name</tt> if reload_dependencies is true or requires if it's false. + def require_or_load(file_name) + reload_dependencies ? load("#{file_name}.rb") : require(file_name) + end + end + + public + # Extracts the action_name from the request parameters and performs that action. + def process(request, response) #:nodoc: + initialize_template_class(response) + assign_shortcuts(request, response) + initialize_current_url + + log_processing unless logger.nil? + perform_action + close_session + + return @response + end + + # Returns an URL that has been rewritten according to the hash of +options+ (for doing a complete redirect, use redirect_to). The + # valid keys in options are specified below with an example going from "/library/books/ISBN/0743536703/show" (mapped to + # books_controller?action=show&type=ISBN&id=0743536703): + # + # .---> controller .--> action + # /library/books/ISBN/0743536703/show + # '------> '--------------> action_prefix + # controller_prefix + # + # * <tt>:controller_prefix</tt> - specifies the string before the controller name, which would be "/library" for the example. + # Called with "/shop" gives "/shop/books/ISBN/0743536703/show". + # * <tt>:controller</tt> - specifies a new controller and clears out everything after the controller name (including the action, + # the pre- and suffix, and all params), so called with "settings" gives "/library/settings/". + # * <tt>:action_prefix</tt> - specifies the string between the controller name and the action name, which would + # be "/ISBN/0743536703" for the example. Called with "/XTC/123/" gives "/library/books/XTC/123/show". + # * <tt>:action</tt> - specifies a new action, so called with "edit" gives "/library/books/ISBN/0743536703/edit" + # * <tt>:action_suffix</tt> - specifies the string after the action name, which would be empty for the example. + # Called with "/detailed" gives "/library/books/ISBN/0743536703/detailed". + # * <tt>:path_params</tt> - specifies a hash that contains keys mapping to the request parameter names. In the example, + # { "type" => "ISBN", "id" => "0743536703" } would be the path_params. It serves as another way of replacing part of + # the action_prefix or action_suffix. So passing { "type" => "XTC" } would give "/library/books/XTC/0743536703/show". + # * <tt>:id</tt> - shortcut where ":id => 5" can be used instead of specifying :path_params => { "id" => 5 }. + # Called with "123" gives "/library/books/ISBN/123/show". + # * <tt>:params</tt> - specifies a hash that represents the regular request parameters, such as { "cat" => 1, + # "origin" => "there"} that would give "?cat=1&origin=there". Called with { "temporary" => 1 } in the example would give + # "/library/books/ISBN/0743536703/show?temporary=1" + # * <tt>:anchor</tt> - specifies the anchor name to be appended to the path. Called with "x14" would give + # "/library/books/ISBN/0743536703/show#x14" + # * <tt>:only_path</tt> - if true, returns the absolute URL (omitting the protocol, host name, and port). + # + # Naturally, you can combine multiple options in a single redirect. Examples: + # + # redirect_to(:controller_prefix => "/shop", :controller => "settings") + # redirect_to(:action => "edit", :id => 3425) + # redirect_to(:action => "edit", :path_params => { "type" => "XTC" }, :params => { "temp" => 1}) + # redirect_to(:action => "publish", :action_prefix => "/published", :anchor => "x14") + # + # Instead of passing an options hash, you can also pass a method reference in the form of a symbol. Consider this example: + # + # class WeblogController < ActionController::Base + # def update + # # do some update + # redirect_to :dashboard_url + # end + # + # protected + # def dashboard_url + # url_for :controller => (@project.active? ? "project" : "account"), :action => "dashboard" + # end + # end + def url_for(options = {}, *parameters_for_method_reference) #:doc: + case options + when String then options + when Symbol then send(options, *parameters_for_method_reference) + when Hash then @url.rewrite(rewrite_options(options)) + end + end + + def module_name + @params["module"] + end + + # Converts the class name from something like "OneModule::TwoModule::NeatController" to "NeatController". + def controller_class_name + self.class.controller_class_name + end + + # Converts the class name from something like "OneModule::TwoModule::NeatController" to "neat". + def controller_name + self.class.controller_name + end + + # Returns the name of the action this controller is processing. + def action_name + @params["action"] || "index" + end + + protected + # Renders the template specified by <tt>template_name</tt>, which defaults to the name of the current controller and action. + # So calling +render+ in WeblogController#show will attempt to render "#{template_root}/weblog/show.rhtml" or + # "#{template_root}/weblog/show.rxml" (in that order). The template_root is set on the ActionController::Base class and is + # shared by all controllers. It's also possible to pass a status code using the second parameter. This defaults to "200 OK", + # but can be changed, such as by calling <tt>render("weblog/error", "500 Error")</tt>. + def render(template_name = nil, status = nil) #:doc: + render_file(template_name || default_template_name, status, true) + end + + # Works like render, but instead of requiring a full template name, you can get by with specifying the action name. So calling + # <tt>render_action "show_many"</tt> in WeblogController#display will render "#{template_root}/weblog/show_many.rhtml" or + # "#{template_root}/weblog/show_many.rxml". + def render_action(action_name, status = nil) #:doc: + render default_template_name(action_name), status + end + + # Works like render, but disregards the template_root and requires a full path to the template that needs to be rendered. Can be + # used like <tt>render_file "/Users/david/Code/Ruby/template"</tt> to render "/Users/david/Code/Ruby/template.rhtml" or + # "/Users/david/Code/Ruby/template.rxml". + def render_file(template_path, status = nil, use_full_path = false) #:doc: + assert_existance_of_template_file(template_path) if use_full_path + logger.info("Rendering #{template_path} (#{status || DEFAULT_RENDER_STATUS_CODE})") unless logger.nil? + + add_variables_to_assigns + render_text(@template.render_file(template_path, use_full_path), status) + end + + # Renders the +template+ string, which is useful for rendering short templates you don't want to bother having a file for. So + # you'd call <tt>render_template "Hello, <%= @user.name %>"</tt> to greet the current user. Or if you want to render as Builder + # template, you could do <tt>render_template "xml.h1 @user.name", nil, "rxml"</tt>. + def render_template(template, status = nil, type = "rhtml") #:doc: + add_variables_to_assigns + render_text(@template.render_template(type, template), status) + end + + # Renders the +text+ string without parsing it through any template engine. Useful for rendering static information as it's + # considerably faster than rendering through the template engine. + # Use block for response body if provided (useful for deferred rendering or streaming output). + def render_text(text = nil, status = nil, &block) #:doc: + add_variables_to_assigns + @response.headers["Status"] = status || DEFAULT_RENDER_STATUS_CODE + @response.body = block_given? ? block : text + @performed_render = true + end + + # Sends the file by streaming it 4096 bytes at a time. This way the + # whole file doesn't need to be read into memory at once. This makes + # it feasible to send even large files. + # + # Be careful to sanitize the path parameter if it coming from a web + # page. send_file(@params['path']) allows a malicious user to + # download any file on your server. + # + # Options: + # * <tt>:filename</tt> - suggests a filename for the browser to use. + # Defaults to File.basename(path). + # * <tt>:type</tt> - specifies an HTTP content type. + # Defaults to 'application/octet-stream'. + # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. + # Valid values are 'inline' and 'attachment' (default). + # * <tt>:streaming</tt> - whether to send the file to the user agent as it is read (true) + # or to read the entire file before sending (false). Defaults to true. + # * <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used to stream the file. + # Defaults to 4096. + # + # The default Content-Type and Content-Disposition headers are + # set to download arbitrary binary files in as many browsers as + # possible. IE versions 4, 5, 5.5, and 6 are all known to have + # a variety of quirks (especially when downloading over SSL). + # + # Simple download: + # send_file '/path/to.zip' + # + # Show a JPEG in browser: + # send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline' + # + # Read about the other Content-* HTTP headers if you'd like to + # provide the user with more information (such as Content-Description). + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11 + # + # Also be aware that the document may be cached by proxies and browsers. + # The Pragma and Cache-Control headers declare how the file may be cached + # by intermediaries. They default to require clients to validate with + # the server before releasing cached responses. See + # http://www.mnot.net/cache_docs/ for an overview of web caching and + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 + # for the Cache-Control header spec. + def send_file(path, options = {}) + raise MissingFile unless File.file?(path) and File.readable?(path) + + options[:length] ||= File.size(path) + options[:filename] ||= File.basename(path) + send_file_headers! options + + if options[:stream] + render_text do + logger.info "Streaming file #{path}" unless logger.nil? + len = options[:buffer_size] || 4096 + File.open(path, 'rb') do |file| + begin + while true + $stdout.syswrite file.sysread(len) + end + rescue EOFError + end + end + end + else + logger.info "Sending file #{path}" unless logger.nil? + File.open(path, 'rb') { |file| render_text file.read } + end + end + + # Send binary data to the user as a file download. May set content type, apparent file name, + # and specify whether to show data inline or download as an attachment. + # + # Options: + # * <tt>:filename</tt> - Suggests a filename for the browser to use. + # * <tt>:type</tt> - specifies an HTTP content type. + # Defaults to 'application/octet-stream'. + # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. + # Valid values are 'inline' and 'attachment' (default). + # + # Generic data download: + # send_data buffer + # + # Download a dynamically-generated tarball: + # send_data generate_tgz('dir'), :filename => 'dir.tgz' + # + # Display an image Active Record in the browser: + # send_data image.data, :type => image.content_type, :disposition => 'inline' + # + # See +send_file+ for more information on HTTP Content-* headers and caching. + def send_data(data, options = {}) + logger.info "Sending data #{options[:filename]}" unless logger.nil? + send_file_headers! options.merge(:length => data.size) + render_text data + end + + def rewrite_options(options) + if defaults = default_url_options(options) + defaults.merge(options) + else + options + end + end + + # Overwrite to implement a number of default options that all url_for-based methods will use. The default options should come in + # the form of a hash, just like the one you would use for url_for directly. Example: + # + # def default_url_options(options) + # { :controller_prefix => @project.active? ? "projects/" : "accounts/" } + # end + # + # As you can infer from the example, this is mostly useful for situations where you want to centralize dynamic decisions about the + # urls as they stem from the business domain. Please note that any individual url_for call can always override the defaults set + # by this method. + def default_url_options(options) #:doc: + end + + # Redirects the browser to an URL that has been rewritten according to the hash of +options+ using a "302 Moved" HTTP header. + # See url_for for a description of the valid options. + def redirect_to(options = {}, *parameters_for_method_reference) #:doc: + if parameters_for_method_reference.empty? + @response.redirected_to = options + redirect_to_url(url_for(options)) + else + @response.redirected_to, @response.redirected_to_method_params = options, parameters_for_method_reference + redirect_to_url(url_for(options, *parameters_for_method_reference)) + end + end + + # Redirects the browser to the specified <tt>path</tt> within the current host (specified with a leading /). Used to sidestep + # the URL rewriting and go directly to a known path. Example: <tt>redirect_to_path "/images/screenshot.jpg"</tt>. + def redirect_to_path(path) #:doc: + redirect_to_url(@request.protocol + @request.host_with_port + path) + end + + # Redirects the browser to the specified <tt>url</tt>. Used to redirect outside of the current application. Example: + # <tt>redirect_to_url "http://www.rubyonrails.org"</tt>. + def redirect_to_url(url) #:doc: + logger.info("Redirected to #{url}") unless logger.nil? + @response.redirect(url) + @performed_redirect = true + end + + # Creates a new cookie that is sent along-side the next render or redirect command. API is the same as for CGI::Cookie. + # Examples: + # + # cookie("name", "value1", "value2", ...) + # cookie("name" => "name", "value" => "value") + # cookie('name' => 'name', + # 'value' => ['value1', 'value2', ...], + # 'path' => 'path', # optional + # 'domain' => 'domain', # optional + # 'expires' => Time.now, # optional + # 'secure' => true # optional + # ) + def cookie(*options) #:doc: + @response.headers["cookie"] << CGI::Cookie.new(*options) + end + + # Resets the session by clearsing out all the objects stored within and initializing a new session object. + def reset_session #:doc: + @request.reset_session + @session = @request.session + @response.session = @session + end + + private + def initialize_template_class(response) + begin + response.template = template_class.new(template_root, {}, self) + rescue + raise "You must assign a template class through ActionController.template_class= before processing a request" + end + + @performed_render = @performed_redirect = false + end + + def assign_shortcuts(request, response) + @request, @params, @cookies = request, request.parameters, request.cookies + + @response = response + @response.session = request.session + + @session = @response.session + @template = @response.template + @assigns = @response.template.assigns + @headers = @response.headers + end + + def initialize_current_url + @url = UrlRewriter.new(@request, controller_name, action_name) + end + + def log_processing + logger.info "\n\nProcessing #{controller_class_name}\##{action_name} (for #{request_origin})" + logger.info " Parameters: #{@params.inspect}" + end + + def perform_action + if action_methods.include?(action_name) + send(action_name) + render unless @performed_render || @performed_redirect + elsif template_exists? && template_public? + render + else + raise UnknownAction, "No action responded to #{action_name}", caller + end + end + + def action_methods + action_controller_classes = self.class.ancestors.reject{ |a| [Object, Kernel].include?(a) } + action_controller_classes.inject([]) { |action_methods, klass| action_methods + klass.instance_methods(false) } + end + + def add_variables_to_assigns + add_instance_variables_to_assigns + add_class_variables_to_assigns if view_controller_internals + end + + def add_instance_variables_to_assigns + protected_variables_cache = protected_instance_variables + instance_variables.each do |var| + next if protected_variables_cache.include?(var) + @assigns[var[1..-1]] = instance_variable_get(var) + end + end + + def add_class_variables_to_assigns + %w( template_root logger template_class ignore_missing_templates ).each do |cvar| + @assigns[cvar] = self.send(cvar) + end + end + + def protected_instance_variables + if view_controller_internals + [ "@assigns", "@performed_redirect", "@performed_render" ] + else + [ "@assigns", "@performed_redirect", "@performed_render", "@request", "@response", "@session", "@cookies", "@template" ] + end + end + + def request_origin + "#{@request.remote_ip} at #{Time.now.to_s}" + end + + def close_session + @session.close unless @session.nil? || Hash === @session + end + + def template_exists?(template_name = default_template_name) + @template.file_exists?(template_name) + end + + def template_public?(template_name = default_template_name) + @template.file_public?(template_name) + end + + def assert_existance_of_template_file(template_name) + unless template_exists?(template_name) || ignore_missing_templates + full_template_path = @template.send(:full_template_path, template_name, 'rhtml') + template_type = (template_name =~ /layouts/i) ? 'layout' : 'template' + raise(MissingTemplate, "Missing #{template_type} #{full_template_path}") + end + end + + def send_file_headers!(options) + options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options)) + [:length, :type, :disposition].each do |arg| + raise ArgumentError, ":#{arg} option required" if options[arg].nil? + end + + disposition = options[:disposition] || 'attachment' + disposition <<= %(; filename="#{options[:filename]}") if options[:filename] + + @headers.update( + 'Content-Length' => options[:length], + 'Content-Type' => options[:type], + 'Content-Disposition' => disposition, + 'Content-Transfer-Encoding' => 'binary' + ); + end + + def default_template_name(default_action_name = action_name) + module_name ? "#{module_name}/#{controller_name}/#{default_action_name}" : "#{controller_name}/#{default_action_name}" + end + end +end diff --git a/actionpack/lib/action_controller/benchmarking.rb b/actionpack/lib/action_controller/benchmarking.rb new file mode 100644 index 0000000000..e6ff65e150 --- /dev/null +++ b/actionpack/lib/action_controller/benchmarking.rb @@ -0,0 +1,49 @@ +require 'benchmark' + +module ActionController #:nodoc: + # The benchmarking module times the performance of actions and reports to the logger. If the Active Record + # package has been included, a separate timing section for database calls will be added as well. + module Benchmarking #:nodoc: + def self.append_features(base) + super + base.class_eval { + alias_method :perform_action_without_benchmark, :perform_action + alias_method :perform_action, :perform_action_with_benchmark + + alias_method :render_without_benchmark, :render + alias_method :render, :render_with_benchmark + } + end + + def render_with_benchmark(template_name = default_template_name, status = "200 OK") + if logger.nil? + render_without_benchmark(template_name, status) + else + @rendering_runtime = Benchmark::measure{ render_without_benchmark(template_name, status) }.real + end + end + + def perform_action_with_benchmark + if logger.nil? + perform_action_without_benchmark + else + runtime = [Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001].max + log_message = "Completed in #{sprintf("%4f", runtime)} (#{(1 / runtime).floor} reqs/sec)" + log_message << rendering_runtime(runtime) if @rendering_runtime + log_message << active_record_runtime(runtime) if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected? + logger.info(log_message) + end + end + + private + def rendering_runtime(runtime) + " | Rendering: #{sprintf("%f", @rendering_runtime)} (#{sprintf("%d", (@rendering_runtime / runtime) * 100)}%)" + end + + def active_record_runtime(runtime) + db_runtime = ActiveRecord::Base.connection.reset_runtime + db_percentage = (db_runtime / runtime) * 100 + " | DB: #{sprintf("%f", db_runtime)} (#{sprintf("%d", db_percentage)}%)" + end + end +end diff --git a/actionpack/lib/action_controller/cgi_ext/cgi_ext.rb b/actionpack/lib/action_controller/cgi_ext/cgi_ext.rb new file mode 100755 index 0000000000..371ead695b --- /dev/null +++ b/actionpack/lib/action_controller/cgi_ext/cgi_ext.rb @@ -0,0 +1,43 @@ +require 'cgi' +require 'cgi/session' +require 'cgi/session/pstore' +require 'action_controller/cgi_ext/cgi_methods' + +# Wrapper around the CGIMethods that have been secluded to allow testing without +# an instatiated CGI object +class CGI #:nodoc: + class << self + alias :escapeHTML_fail_on_nil :escapeHTML + + def escapeHTML(string) + escapeHTML_fail_on_nil(string) unless string.nil? + end + end + + # Returns a parameter hash including values from both the request (POST/GET) + # and the query string with the latter taking precedence. + def parameters + request_parameters.update(query_parameters) + end + + def query_parameters + CGIMethods.parse_query_parameters(query_string) + end + + def request_parameters + CGIMethods.parse_request_parameters(params) + end + + def redirect(where) + header({ + "Status" => "302 Moved", + "location" => "#{where}" + }) + end + + def session(parameters = nil) + parameters = {} if parameters.nil? + parameters['database_manager'] = CGI::Session::PStore + CGI::Session.new(self, parameters) + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb b/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb new file mode 100755 index 0000000000..261490580c --- /dev/null +++ b/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb @@ -0,0 +1,91 @@ +require 'cgi' + +# Static methods for parsing the query and request parameters that can be used in +# a CGI extension class or testing in isolation. +class CGIMethods #:nodoc: + public + # Returns a hash with the pairs from the query string. The implicit hash construction that is done in + # parse_request_params is not done here. + def CGIMethods.parse_query_parameters(query_string) + parsed_params = {} + + query_string.split(/[&;]/).each { |p| + k, v = p.split('=') + + k = CGI.unescape(k) unless k.nil? + v = CGI.unescape(v) unless v.nil? + + if k =~ /(.*)\[\]$/ + if parsed_params.has_key? $1 + parsed_params[$1] << v + else + parsed_params[$1] = [v] + end + else + parsed_params[k] = v.nil? ? nil : v + end + } + + return parsed_params + end + + # Returns the request (POST/GET) parameters in a parsed form where pairs such as "customer[address][street]" / + # "Somewhere cool!" are translated into a full hash hierarchy, like + # { "customer" => { "address" => { "street" => "Somewhere cool!" } } } + def CGIMethods.parse_request_parameters(params) + parsed_params = {} + + for key, value in params + value = [value] if key =~ /.*\[\]$/ + CGIMethods.build_deep_hash( + CGIMethods.get_typed_value(value[0]), + parsed_params, + CGIMethods.get_levels(key) + ) + end + + return parsed_params + end + + private + def CGIMethods.get_typed_value(value) + if value.respond_to?(:content_type) && !value.content_type.empty? + # Uploaded file + value + elsif value.respond_to?(:read) + # Value as part of a multipart request + value.read + elsif value.class == Array + value + else + # Standard value (not a multipart request) + value.to_s + end + end + + def CGIMethods.get_levels(key_string) + return [] if key_string.nil? or key_string.empty? + + levels = [] + main, existance = /(\w+)(\[)?.?/.match(key_string).captures + levels << main + + unless existance.nil? + hash_part = key_string.sub(/\w+\[/, "") + hash_part.slice!(-1, 1) + levels += hash_part.split(/\]\[/) + end + + levels + end + + def CGIMethods.build_deep_hash(value, hash, levels) + if levels.length == 0 + value; + elsif hash.nil? + { levels.first => CGIMethods.build_deep_hash(value, nil, levels[1..-1]) } + else + hash.update({ levels.first => CGIMethods.build_deep_hash(value, hash[levels.first], levels[1..-1]) }) + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/cgi_process.rb b/actionpack/lib/action_controller/cgi_process.rb new file mode 100644 index 0000000000..e69a0b2f8d --- /dev/null +++ b/actionpack/lib/action_controller/cgi_process.rb @@ -0,0 +1,124 @@ +require 'action_controller/cgi_ext/cgi_ext' +require 'action_controller/support/cookie_performance_fix' +require 'action_controller/session/drb_store' +require 'action_controller/session/active_record_store' + +module ActionController #:nodoc: + class Base + # Process a request extracted from an CGI object and return a response. Pass false as <tt>session_options</tt> to disable + # sessions (large performance increase if sessions are not needed). The <tt>session_options</tt> are the same as for CGI::Session: + # + # * <tt>:database_manager</tt> - standard options are CGI::Session::FileStore, CGI::Session::MemoryStore, and CGI::Session::PStore + # (default). Additionally, there is CGI::Session::DRbStore and CGI::Session::ActiveRecordStore. Read more about these in + # lib/action_controller/session. + # * <tt>:session_key</tt> - the parameter name used for the session id. Defaults to '_session_id'. + # * <tt>:session_id</tt> - the session id to use. If not provided, then it is retrieved from the +session_key+ parameter + # of the request, or automatically generated for a new session. + # * <tt>:new_session</tt> - if true, force creation of a new session. If not set, a new session is only created if none currently + # exists. If false, a new session is never created, and if none currently exists and the +session_id+ option is not set, + # an ArgumentError is raised. + # * <tt>:session_expires</tt> - the time the current session expires, as a +Time+ object. If not set, the session will continue + # indefinitely. + # * <tt>:session_domain</tt> - the hostname domain for which this session is valid. If not set, defaults to the hostname of the + # server. + # * <tt>:session_secure</tt> - if +true+, this session will only work over HTTPS. + # * <tt>:session_path</tt> - the path for which this session applies. Defaults to the directory of the CGI script. + def self.process_cgi(cgi = CGI.new, session_options = {}) + new.process_cgi(cgi, session_options) + end + + def process_cgi(cgi, session_options = {}) #:nodoc: + process(CgiRequest.new(cgi, session_options), CgiResponse.new(cgi)).out + end + end + + class CgiRequest < AbstractRequest #:nodoc: + attr_accessor :cgi + + DEFAULT_SESSION_OPTIONS = + { "database_manager" => CGI::Session::PStore, "prefix" => "ruby_sess.", "session_path" => "/" } + + def initialize(cgi, session_options = {}) + @cgi = cgi + @session_options = session_options + super() + end + + def query_parameters + @cgi.query_string ? CGIMethods.parse_query_parameters(@cgi.query_string) : {} + end + + def request_parameters + CGIMethods.parse_request_parameters(@cgi.params) + end + + def env + @cgi.send(:env_table) + end + + def cookies + @cgi.cookies.freeze + end + + def host + env["HTTP_X_FORWARDED_HOST"] || @cgi.host.split(":").first + end + + def session + return @session unless @session.nil? + begin + @session = (@session_options == false ? {} : CGI::Session.new(@cgi, DEFAULT_SESSION_OPTIONS.merge(@session_options))) + @session["__valid_session"] + return @session + rescue ArgumentError => e + @session.delete if @session + raise( + ActionController::SessionRestoreError, + "Session contained objects where the class definition wasn't available. " + + "Remember to require classes for all objects kept in the session. " + + "The session has been deleted." + ) + end + end + + def reset_session + @session.delete + @session = (@session_options == false ? {} : new_session) + end + + def method_missing(method_id, *arguments) + @cgi.send(method_id, *arguments) rescue super + end + + private + def new_session + CGI::Session.new(@cgi, DEFAULT_SESSION_OPTIONS.merge(@session_options).merge("new_session" => true)) + end + end + + class CgiResponse < AbstractResponse #:nodoc: + def initialize(cgi) + @cgi = cgi + super() + end + + def out + convert_content_type!(@headers) + $stdout.binmode if $stdout.respond_to?(:binmode) + print @cgi.header(@headers) + if @body.respond_to?(:call) + @body.call(self) + else + print @body + end + end + + private + def convert_content_type!(headers) + if headers["Content-Type"] + headers["type"] = headers["Content-Type"] + headers.delete "Content-Type" + end + end + end +end diff --git a/actionpack/lib/action_controller/dependencies.rb b/actionpack/lib/action_controller/dependencies.rb new file mode 100644 index 0000000000..6f092500d1 --- /dev/null +++ b/actionpack/lib/action_controller/dependencies.rb @@ -0,0 +1,49 @@ +module ActionController #:nodoc: + module Dependencies #:nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + end + + module ClassMethods + def model(*models) + require_dependencies(:model, models) + depend_on(:model, models) + end + + def service(*services) + require_dependencies(:service, services) + depend_on(:service, services) + end + + def observer(*observers) + require_dependencies(:observer, observers) + depend_on(:observer, observers) + instantiate_observers(observers) + end + + def dependencies_on(layer) # :nodoc: + read_inheritable_attribute("#{layer}_dependencies") + end + + def depend_on(layer, dependencies) + write_inheritable_array("#{layer}_dependencies", dependencies) + end + + private + def instantiate_observers(observers) + observers.flatten.each { |observer| Object.const_get(Inflector.classify(observer.to_s)).instance } + end + + def require_dependencies(layer, dependencies) + dependencies.flatten.each do |dependency| + begin + require_or_load(dependency.to_s) + rescue LoadError + raise LoadError, "Missing #{layer} #{dependency}.rb" + end + end + end + end + end +end diff --git a/actionpack/lib/action_controller/filters.rb b/actionpack/lib/action_controller/filters.rb new file mode 100644 index 0000000000..bd5c545dfb --- /dev/null +++ b/actionpack/lib/action_controller/filters.rb @@ -0,0 +1,279 @@ +module ActionController #:nodoc: + module Filters #:nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + base.class_eval { include ActionController::Filters::InstanceMethods } + end + + # Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do + # authentication, caching, or auditing before the intended action is performed. Or to do localization or output + # compression after the action has been performed. + # + # Filters have access to the request, response, and all the instance variables set by other filters in the chain + # or by the action (in the case of after filters). Additionally, it's possible for a pre-processing <tt>before_filter</tt> + # to halt the processing before the intended action is processed by returning false. This is especially useful for + # filters like authentication where you're not interested in allowing the action to be performed if the proper + # credentials are not in order. + # + # == Filter inheritance + # + # Controller inheritance hierarchies share filters downwards, but subclasses can also add new filters without + # affecting the superclass. For example: + # + # class BankController < ActionController::Base + # before_filter :audit + # + # private + # def audit + # # record the action and parameters in an audit log + # end + # end + # + # class VaultController < BankController + # before_filter :verify_credentials + # + # private + # def verify_credentials + # # make sure the user is allowed into the vault + # end + # end + # + # Now any actions performed on the BankController will have the audit method called before. On the VaultController, + # first the audit method is called, then the verify_credentials method. If the audit method returns false, then + # verify_credentials and the intended action is never called. + # + # == Filter types + # + # A filter can take one of three forms: method reference (symbol), external class, or inline method (proc). The first + # is the most common and works by referencing a protected or private method somewhere in the inheritance hierarchy of + # the controller by use of a symbol. In the bank example above, both BankController and VaultController use this form. + # + # Using an external class makes for more easily reused generic filters, such as output compression. External filter classes + # are implemented by having a static +filter+ method on any class and then passing this class to the filter method. Example: + # + # class OutputCompressionFilter + # def self.filter(controller) + # controller.response.body = compress(controller.response.body) + # end + # end + # + # class NewspaperController < ActionController::Base + # after_filter OutputCompressionFilter + # end + # + # The filter method is passed the controller instance and is hence granted access to all aspects of the controller and can + # manipulate them as it sees fit. + # + # The inline method (using a proc) can be used to quickly do something small that doesn't require a lot of explanation. + # Or just as a quick test. It works like this: + # + # class WeblogController < ActionController::Base + # before_filter { |controller| return false if controller.params["stop_action"] } + # end + # + # As you can see, the block expects to be passed the controller after it has assigned the request to the internal variables. + # This means that the block has access to both the request and response objects complete with convenience methods for params, + # session, template, and assigns. Note: The inline method doesn't strictly has to be a block. Any object that responds to call + # and returns 1 or -1 on arity will do (such as a Proc or an Method object). + # + # == Filter chain ordering + # + # Using <tt>before_filter</tt> and <tt>after_filter</tt> appends the specified filters to the existing chain. That's usually + # just fine, but some times you care more about the order in which the filters are executed. When that's the case, you + # can use <tt>prepend_before_filter</tt> and <tt>prepend_after_filter</tt>. Filters added by these methods will be put at the + # beginning of their respective chain and executed before the rest. For example: + # + # class ShoppingController + # before_filter :verify_open_shop + # + # class CheckoutController + # prepend_before_filter :ensure_items_in_cart, :ensure_items_in_stock + # + # The filter chain for the CheckoutController is now <tt>:ensure_items_in_cart, :ensure_items_in_stock,</tt> + # <tt>:verify_open_shop</tt>. So if either of the ensure filters return false, we'll never get around to see if the shop + # is open or not. + # + # You may pass multiple filter arguments of each type as well as a filter block. + # If a block is given, it is treated as the last argument. + # + # == Around filters + # + # In addition to the individual before and after filters, it's also possible to specify that a single object should handle + # both the before and after call. That's especially usefuly when you need to keep state active between the before and after, + # such as the example of a benchmark filter below: + # + # class WeblogController < ActionController::Base + # around_filter BenchmarkingFilter.new + # + # # Before this action is performed, BenchmarkingFilter#before(controller) is executed + # def index + # end + # # After this action has been performed, BenchmarkingFilter#after(controller) is executed + # end + # + # class BenchmarkingFilter + # def initialize + # @runtime + # end + # + # def before + # start_timer + # end + # + # def after + # stop_timer + # report_result + # end + # end + module ClassMethods + # The passed <tt>filters</tt> will be appended to the array of filters that's run _before_ actions + # on this controller are performed. + def append_before_filter(*filters, &block) + filters << block if block_given? + append_filter_to_chain("before", filters) + end + + # The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions + # on this controller are performed. + def prepend_before_filter(*filters, &block) + filters << block if block_given? + prepend_filter_to_chain("before", filters) + end + + # Short-hand for append_before_filter since that's the most common of the two. + alias :before_filter :append_before_filter + + # The passed <tt>filters</tt> will be appended to the array of filters that's run _after_ actions + # on this controller are performed. + def append_after_filter(*filters, &block) + filters << block if block_given? + append_filter_to_chain("after", filters) + end + + # The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions + # on this controller are performed. + def prepend_after_filter(*filters, &block) + filters << block if block_given? + prepend_filter_to_chain("after", filters) + end + + # Short-hand for append_after_filter since that's the most common of the two. + alias :after_filter :append_after_filter + + # The passed <tt>filters</tt> will have their +before+ method appended to the array of filters that's run both before actions + # on this controller are performed and have their +after+ method prepended to the after actions. The filter objects must all + # respond to both +before+ and +after+. So if you do append_around_filter A.new, B.new, the callstack will look like: + # + # B#before + # A#before + # A#after + # B#after + def append_around_filter(filters) + for filter in [filters].flatten + ensure_filter_responds_to_before_and_after(filter) + append_before_filter { |c| filter.before(c) } + prepend_after_filter { |c| filter.after(c) } + end + end + + # The passed <tt>filters</tt> will have their +before+ method prepended to the array of filters that's run both before actions + # on this controller are performed and have their +after+ method appended to the after actions. The filter objects must all + # respond to both +before+ and +after+. So if you do prepend_around_filter A.new, B.new, the callstack will look like: + # + # A#before + # B#before + # B#after + # A#after + def prepend_around_filter(filters) + for filter in [filters].flatten + ensure_filter_responds_to_before_and_after(filter) + prepend_before_filter { |c| filter.before(c) } + append_after_filter { |c| filter.after(c) } + end + end + + # Short-hand for append_around_filter since that's the most common of the two. + alias :around_filter :append_around_filter + + # Returns all the before filters for this class and all its ancestors. + def before_filters #:nodoc: + read_inheritable_attribute("before_filters") + end + + # Returns all the after filters for this class and all its ancestors. + def after_filters #:nodoc: + read_inheritable_attribute("after_filters") + end + + private + def append_filter_to_chain(condition, filters) + write_inheritable_array("#{condition}_filters", filters) + end + + def prepend_filter_to_chain(condition, filters) + write_inheritable_attribute("#{condition}_filters", filters + read_inheritable_attribute("#{condition}_filters")) + end + + def ensure_filter_responds_to_before_and_after(filter) + unless filter.respond_to?(:before) && filter.respond_to?(:after) + raise ActionControllerError, "Filter object must respond to both before and after" + end + end + end + + module InstanceMethods # :nodoc: + def self.append_features(base) + super + base.class_eval { + alias_method :perform_action_without_filters, :perform_action + alias_method :perform_action, :perform_action_with_filters + } + end + + def perform_action_with_filters + return if before_action == false + perform_action_without_filters + after_action + end + + # Calls all the defined before-filter filters, which are added by using "before_filter :method". + # If any of the filters return false, no more filters will be executed and the action is aborted. + def before_action #:doc: + call_filters(self.class.before_filters) + end + + # Calls all the defined after-filter filters, which are added by using "after_filter :method". + # If any of the filters return false, no more filters will be executed. + def after_action #:doc: + call_filters(self.class.after_filters) + end + + private + def call_filters(filters) + filters.each do |filter| + if Symbol === filter + if self.send(filter) == false then return false end + elsif filter_block?(filter) + if filter.call(self) == false then return false end + elsif filter_class?(filter) + if filter.filter(self) == false then return false end + else + raise( + ActionControllerError, + "Filters need to be either a symbol, proc/method, or class implementing a static filter method" + ) + end + end + end + + def filter_block?(filter) + filter.respond_to?("call") && (filter.arity == 1 || filter.arity == -1) + end + + def filter_class?(filter) + filter.respond_to?("filter") + end + end + end +end diff --git a/actionpack/lib/action_controller/flash.rb b/actionpack/lib/action_controller/flash.rb new file mode 100644 index 0000000000..220ed8c77a --- /dev/null +++ b/actionpack/lib/action_controller/flash.rb @@ -0,0 +1,65 @@ +module ActionController #:nodoc: + # The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed + # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create action + # that sets <tt>flash["notice"] = "Succesfully created"</tt> before redirecting to a display action that can then expose + # the flash to its template. Actually, that exposure is automatically done. Example: + # + # class WeblogController < ActionController::Base + # def create + # # save post + # flash["notice"] = "Succesfully created post" + # redirect_to :action => "display", :params => { "id" => post.id } + # end + # + # def display + # # doesn't need to assign the flash notice to the template, that's done automatically + # end + # end + # + # display.rhtml + # <% if @flash["notice"] %><div class="notice"><%= @flash["notice"] %></div><% end %> + # + # This example just places a string in the flash, but you can put any object in there. And of course, you can put as many + # as you like at a time too. Just remember: They'll be gone by the time the next action has been performed. + module Flash + def self.append_features(base) #:nodoc: + super + base.before_filter(:fire_flash) + base.after_filter(:clear_flash) + end + + protected + # Access the contents of the flash. Use <tt>flash["notice"]</tt> to read a notice you put there or + # <tt>flash["notice"] = "hello"</tt> to put a new one. + def flash #:doc: + if @session["flash"].nil? + @session["flash"] = {} + @session["flashes"] ||= 0 + end + @session["flash"] + end + + # Can be called by any action that would like to keep the current content of the flash around for one more action. + def keep_flash #:doc: + @session["flashes"] = 0 + end + + private + # Records that the contents of @session["flash"] was flashed to the action + def fire_flash + if @session["flash"] + @session["flashes"] += 1 unless @session["flash"].empty? + @assigns["flash"] = @session["flash"] + else + @assigns["flash"] = {} + end + end + + def clear_flash + if @session["flash"] && (@session["flashes"].nil? || @session["flashes"] >= 1) + @session["flash"] = {} + @session["flashes"] = 0 + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/helpers.rb b/actionpack/lib/action_controller/helpers.rb new file mode 100644 index 0000000000..9c88582288 --- /dev/null +++ b/actionpack/lib/action_controller/helpers.rb @@ -0,0 +1,100 @@ +module ActionController #:nodoc: + module Helpers #:nodoc: + def self.append_features(base) + super + base.class_eval { class << self; alias_method :inherited_without_helper, :inherited; end } + base.extend(ClassMethods) + end + + # The template helpers serves to relieve the templates from including the same inline code again and again. It's a + # set of standardized methods for working with forms (FormHelper), dates (DateHelper), texts (TextHelper), and + # Active Records (ActiveRecordHelper) that's available to all templates by default. + # + # It's also really easy to make your own helpers and it's much encouraged to keep the template files free + # from complicated logic. It's even encouraged to bundle common compositions of methods from other helpers + # (often the common helpers) as they're used by the specific application. + # + # module MyHelper + # def hello_world() "hello world" end + # end + # + # MyHelper can now be included in a controller, like this: + # + # class MyController < ActionController::Base + # helper :my_helper + # end + # + # ...and, same as above, used in any template rendered from MyController, like this: + # + # Let's hear what the helper has to say: <tt><%= hello_world %></tt> + module ClassMethods + # Makes all the (instance) methods in the helper module available to templates rendered through this controller. + # See ActionView::Helpers (link:classes/ActionView/Helpers.html) for more about making your own helper modules + # available to the templates. + def add_template_helper(helper_module) + template_class.class_eval "include #{helper_module}" + end + + # Declare a helper. If you use this method in your controller, you don't + # have to do the +self.append_features+ incantation in your helper class. + # helper :foo + # requires 'foo_helper' and includes FooHelper in the template class. + # helper FooHelper + # includes FooHelper in the template class. + # helper { def foo() "#{bar} is the very best" end } + # evaluates the block in the template class, adding method #foo. + # helper(:three, BlindHelper) { def mice() 'mice' end } + # does all three. + def helper(*args, &block) + args.flatten.each do |arg| + case arg + when Module + add_template_helper(arg) + when String, Symbol + file_name = Inflector.underscore(arg.to_s.downcase) + '_helper' + class_name = Inflector.camelize(file_name) + begin + require_or_load(file_name) + rescue LoadError + raise LoadError, "Missing helper file helpers/#{file_name}.rb" + end + raise ArgumentError, "Missing #{class_name} module in helpers/#{file_name}.rb" unless Object.const_defined?(class_name) + add_template_helper(Object.const_get(class_name)) + else + raise ArgumentError, 'helper expects String, Symbol, or Module argument' + end + end + + # Evaluate block in template class if given. + template_class.module_eval(&block) if block_given? + end + + # Declare a controller method as a helper. For example, + # helper_method :link_to + # def link_to(name, options) ... end + # makes the link_to controller method available in the view. + def helper_method(*methods) + template_class.controller_delegate(*methods) + end + + # Declare a controller attribute as a helper. For example, + # helper_attr :name + # attr_accessor :name + # makes the name and name= controller methods available in the view. + # The is a convenience wrapper for helper_method. + def helper_attr(*attrs) + attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") } + end + + private + def inherited(child) + inherited_without_helper(child) + begin + child.helper(child.controller_name) + rescue LoadError + # No default helper available for this controller + end + end + end + end +end diff --git a/actionpack/lib/action_controller/layout.rb b/actionpack/lib/action_controller/layout.rb new file mode 100644 index 0000000000..7ae25ddabd --- /dev/null +++ b/actionpack/lib/action_controller/layout.rb @@ -0,0 +1,149 @@ +module ActionController #:nodoc: + module Layout #:nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + base.class_eval do + alias_method :render_without_layout, :render + alias_method :render, :render_with_layout + end + end + + # Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in + # repeated setups. The inclusion pattern has pages that look like this: + # + # <%= render "shared/header" %> + # Hello World + # <%= render "shared/footer" %> + # + # This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose + # and if you ever want to change the structure of these two includes, you'll have to change all the templates. + # + # With layouts, you can flip it around and have the common structure know where to insert changing content. This means + # that the header and footer is only mentioned in one place, like this: + # + # <!-- The header part of this layout --> + # <%= @content_for_layout %> + # <!-- The footer part of this layout --> + # + # And then you have content pages that look like this: + # + # hello world + # + # Not a word about common structures. At rendering time, the content page is computed and then inserted in the layout, + # like this: + # + # <!-- The header part of this layout --> + # hello world + # <!-- The footer part of this layout --> + # + # == Accessing shared variables + # + # Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with + # references that won't materialize before rendering time: + # + # <h1><%= @page_title %></h1> + # <%= @content_for_layout %> + # + # ...and content pages that fulfill these references _at_ rendering time: + # + # <% @page_title = "Welcome" %> + # Off-world colonies offers you a chance to start a new life + # + # The result after rendering is: + # + # <h1>Welcome</h1> + # Off-world colonies offers you a chance to start a new life + # + # == Inheritance for layouts + # + # Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples: + # + # class BankController < ActionController::Base + # layout "layouts/bank_standard" + # + # class InformationController < BankController + # + # class VaultController < BankController + # layout :access_level_layout + # + # class EmployeeController < BankController + # layout nil + # + # The InformationController uses "layouts/bank_standard" inherited from the BankController, the VaultController overwrites + # and picks the layout dynamically, and the EmployeeController doesn't want to use a layout at all. + # + # == Types of layouts + # + # Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes + # you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can + # be done either by specifying a method reference as a symbol or using an inline method (as a proc). + # + # The method reference is the preferred approach to variable layouts and is used like this: + # + # class WeblogController < ActionController::Base + # layout :writers_and_readers + # + # def index + # # fetching posts + # end + # + # private + # def writers_and_readers + # logged_in? ? "writer_layout" : "reader_layout" + # end + # + # Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing + # is logged in or not. + # + # If you want to use an inline method, such as a proc, do something like this: + # + # class WeblogController < ActionController::Base + # layout proc{ |controller| controller.logged_in? ? "writer_layout" : "reader_layout" } + # + # Of course, the most common way of specifying a layout is still just as a plain template path: + # + # class WeblogController < ActionController::Base + # layout "layouts/weblog_standard" + # + # == Avoiding the use of a layout + # + # If you have a layout that by default is applied to all the actions of a controller, you still have the option to rendering + # a given action without a layout. Just use the method <tt>render_without_layout</tt>, which works just like Base.render -- + # it just doesn't apply any layouts. + module ClassMethods + # If a layout is specified, all actions rendered through render and render_action will have their result assigned + # to <tt>@content_for_layout</tt>, which can then be used by the layout to insert their contents with + # <tt><%= @content_for_layout %></tt>. This layout can itself depend on instance variables assigned during action + # performance and have access to them as any normal template would. + def layout(template_name) + write_inheritable_attribute "layout", template_name + end + end + + # Returns the name of the active layout. If the layout was specified as a method reference (through a symbol), this method + # is called and the return value is used. Likewise if the layout was specified as an inline method (through a proc or method + # object). If the layout was defined without a directory, layouts is assumed. So <tt>layout "weblog/standard"</tt> will return + # weblog/standard, but <tt>layout "standard"</tt> will return layouts/standard. + def active_layout(passed_layout = nil) + layout = passed_layout || self.class.read_inheritable_attribute("layout") + active_layout = case layout + when Symbol then send(layout) + when Proc then layout.call(self) + when String then layout + end + active_layout.include?("/") ? active_layout : "layouts/#{active_layout}" if active_layout + end + + def render_with_layout(template_name = default_template_name, status = nil, layout = nil) #:nodoc: + if layout ||= active_layout + add_variables_to_assigns + logger.info("Rendering #{template_name} within #{layout}") unless logger.nil? + @content_for_layout = @template.render_file(template_name, true) + render_without_layout(layout, status) + else + render_without_layout(template_name, status) + end + end + end +end diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb new file mode 100755 index 0000000000..1085066ea0 --- /dev/null +++ b/actionpack/lib/action_controller/request.rb @@ -0,0 +1,99 @@ +module ActionController + # These methods are available in both the production and test Request objects. + class AbstractRequest + # Returns both GET and POST parameters in a single hash. + def parameters + @parameters ||= request_parameters.update(query_parameters) + end + + def method + env['REQUEST_METHOD'].downcase.intern + end + + def get? + method == :get + end + + def post? + method == :post + end + + def put? + method == :put + end + + def delete? + method == :delete + end + + # Determine originating IP address. REMOTE_ADDR is the standard + # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or + # HTTP_X_FORWARDED_FOR are set by proxies so check for these before + # falling back to REMOTE_ADDR. HTTP_X_FORWARDED_FOR may be a comma- + # delimited list in the case of multiple chained proxies; the first is + # the originating IP. + def remote_ip + if env['HTTP_CLIENT_IP'] + env['HTTP_CLIENT_IP'] + elsif env['HTTP_X_FORWARDED_FOR'] + remote_ip = env['HTTP_X_FORWARDED_FOR'].split(',').reject { |ip| + ip =~ /^unknown$|^(10|172\.16|192\.168)\./i + }.first + + remote_ip ? remote_ip.strip : env['REMOTE_ADDR'] + else + env['REMOTE_ADDR'] + end + end + + def request_uri + env["REQUEST_URI"] + end + + def protocol + port == 443 ? "https://" : "http://" + end + + def path + request_uri ? request_uri.split("?").first : "" + end + + def port + env["SERVER_PORT"].to_i + end + + def host_with_port + if env['HTTP_HOST'] + env['HTTP_HOST'] + elsif (protocol == "http://" && port == 80) || (protocol == "https://" && port == 443) + host + else + host + ":#{port}" + end + end + + #-- + # Must be implemented in the concrete request + #++ + def query_parameters + end + + def request_parameters + end + + def env + end + + def host + end + + def cookies + end + + def session + end + + def reset_session + end + end +end diff --git a/actionpack/lib/action_controller/rescue.rb b/actionpack/lib/action_controller/rescue.rb new file mode 100644 index 0000000000..c0933b2666 --- /dev/null +++ b/actionpack/lib/action_controller/rescue.rb @@ -0,0 +1,94 @@ +module ActionController #:nodoc: + # Actions that fail to perform as expected throw exceptions. These exceptions can either be rescued for the public view + # (with a nice user-friendly explanation) or for the developers view (with tons of debugging information). The developers view + # is already implemented by the Action Controller, but the public view should be tailored to your specific application. So too + # could the decision on whether something is a public or a developer request. + # + # You can tailor the rescuing behavior and appearance by overwriting the following two stub methods. + module Rescue + def self.append_features(base) #:nodoc: + super + base.class_eval do + alias_method :perform_action_without_rescue, :perform_action + alias_method :perform_action, :perform_action_with_rescue + end + end + + protected + # Exception handler called when the performance of an action raises an exception. + def rescue_action(exception) + log_error(exception) unless logger.nil? + + if consider_all_requests_local || local_request? + rescue_action_locally(exception) + else + rescue_action_in_public(exception) + end + end + + # Overwrite to implement custom logging of errors. By default logs as fatal. + def log_error(exception) #:doc: + if ActionView::TemplateError === exception + logger.fatal(exception.to_s) + else + logger.fatal( + "\n\n#{exception.class} (#{exception.message}):\n " + + clean_backtrace(exception).join("\n ") + + "\n\n" + ) + end + end + + # Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>). + def rescue_action_in_public(exception) #:doc: + render_text "<html><body><h1>Application error (Rails)</h1></body></html>" + end + + # Overwrite to expand the meaning of a local request in order to show local rescues on other occurances than + # the remote IP being 127.0.0.1. For example, this could include the IP of the developer machine when debugging + # remotely. + def local_request? #:doc: + @request.remote_addr == "127.0.0.1" + end + + # Renders a detailed diagnostics screen on action exceptions. + def rescue_action_locally(exception) + @exception = exception + @rescues_path = File.dirname(__FILE__) + "/templates/rescues/" + add_variables_to_assigns + @contents = @template.render_file(template_path_for_local_rescue(exception), false) + + @headers["Content-Type"] = "text/html" + render_file(rescues_path("layout"), "500 Internal Error") + end + + private + def perform_action_with_rescue #:nodoc: + begin + perform_action_without_rescue + rescue Exception => exception + rescue_action(exception) + end + end + + def rescues_path(template_name) + File.dirname(__FILE__) + "/templates/rescues/#{template_name}.rhtml" + end + + def template_path_for_local_rescue(exception) + rescues_path( + case exception + when MissingTemplate then "missing_template" + when UnknownAction then "unknown_action" + when ActionView::TemplateError then "template_error" + else "diagnostics" + end + ) + end + + def clean_backtrace(exception) + base_dir = File.expand_path(File.dirname(__FILE__) + "/../../../../") + exception.backtrace.collect { |line| line.gsub(base_dir, "").gsub("/public/../config/environments/../../", "").gsub("/public/../", "") } + end + end +end diff --git a/actionpack/lib/action_controller/response.rb b/actionpack/lib/action_controller/response.rb new file mode 100755 index 0000000000..836dd5ffef --- /dev/null +++ b/actionpack/lib/action_controller/response.rb @@ -0,0 +1,15 @@ +module ActionController + class AbstractResponse #:nodoc: + DEFAULT_HEADERS = { "Cache-Control" => "no-cache" } + attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params + + def initialize + @body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], [] + end + + def redirect(to_url) + @headers["Status"] = "302 Moved" + @headers["location"] = to_url + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/scaffolding.rb b/actionpack/lib/action_controller/scaffolding.rb new file mode 100644 index 0000000000..49b35b37df --- /dev/null +++ b/actionpack/lib/action_controller/scaffolding.rb @@ -0,0 +1,183 @@ +module ActionController + module Scaffolding # :nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + end + + # Scaffolding is a way to quickly put an Active Record class online by providing a series of standardized actions + # for listing, showing, creating, updating, and destroying objects of the class. These standardized actions come + # with both controller logic and default templates that through introspection already know which fields to display + # and which input types to use. Example: + # + # class WeblogController < ActionController::Base + # scaffold :entry + # end + # + # This tiny piece of code will add all of the following methods to the controller: + # + # class WeblogController < ActionController::Base + # def index + # list + # end + # + # def list + # @entries = Entry.find_all + # render_scaffold "list" + # end + # + # def show + # @entry = Entry.find(@params["id"]) + # render_scaffold + # end + # + # def destroy + # Entry.find(@params["id"]).destroy + # redirect_to :action => "list" + # end + # + # def new + # @entry = Entry.new + # render_scaffold + # end + # + # def create + # @entry = Entry.new(@params["entry"]) + # if @entry.save + # flash["notice"] = "Entry was succesfully created" + # redirect_to :action => "list" + # else + # render "entry/new" + # end + # end + # + # def edit + # @entry = Entry.find(@params["id"]) + # render_scaffold + # end + # + # def update + # @entry = Entry.find(@params["entry"]["id"]) + # @entry.attributes = @params["entry"] + # + # if @entry.save + # flash["notice"] = "Entry was succesfully updated" + # redirect_to :action => "show/" + @entry.id.to_s + # else + # render "entry/edit" + # end + # end + # end + # + # The <tt>render_scaffold</tt> method will first check to see if you've made your own template (like "weblog/show.rhtml" for + # the show action) and if not, then render the generic template for that action. This gives you the possibility of using the + # scaffold while you're building your specific application. Start out with a totally generic setup, then replace one template + # and one action at a time while relying on the rest of the scaffolded templates and actions. + module ClassMethods + # Adds a swath of generic CRUD actions to the controller. The +model_id+ is automatically converted into a class name unless + # one is specifically provide through <tt>options[:class_name]</tt>. So <tt>scaffold :post</tt> would use Post as the class + # and @post/@posts for the instance variables. + # + # It's possible to use more than one scaffold in a single controller by specifying <tt>options[:suffix] = true</tt>. This will + # make <tt>scaffold :post, :suffix => true</tt> use method names like list_post, show_post, and create_post + # instead of just list, show, and post. If suffix is used, then no index method is added. + def scaffold(model_id, options = {}) + validate_options([ :class_name, :suffix ], options.keys) + + require "#{model_id.id2name}" rescue logger.warn "Couldn't auto-require #{model_id.id2name}.rb" unless logger.nil? + + singular_name = model_id.id2name + class_name = options[:class_name] || Inflector.camelize(singular_name) + plural_name = Inflector.pluralize(singular_name) + suffix = options[:suffix] ? "_#{singular_name}" : "" + + unless options[:suffix] + module_eval <<-"end_eval", __FILE__, __LINE__ + def index + list + end + end_eval + end + + module_eval <<-"end_eval", __FILE__, __LINE__ + def list#{suffix} + @#{plural_name} = #{class_name}.find_all + render#{suffix}_scaffold "list#{suffix}" + end + + def show#{suffix} + @#{singular_name} = #{class_name}.find(@params["id"]) + render#{suffix}_scaffold + end + + def destroy#{suffix} + #{class_name}.find(@params["id"]).destroy + redirect_to :action => "list#{suffix}" + end + + def new#{suffix} + @#{singular_name} = #{class_name}.new + render#{suffix}_scaffold + end + + def create#{suffix} + @#{singular_name} = #{class_name}.new(@params["#{singular_name}"]) + if @#{singular_name}.save + flash["notice"] = "#{class_name} was succesfully created" + redirect_to :action => "list#{suffix}" + else + render "#{singular_name}/new#{suffix}" + end + end + + def edit#{suffix} + @#{singular_name} = #{class_name}.find(@params["id"]) + render#{suffix}_scaffold + end + + def update#{suffix} + @#{singular_name} = #{class_name}.find(@params["#{singular_name}"]["id"]) + @#{singular_name}.attributes = @params["#{singular_name}"] + + if @#{singular_name}.save + flash["notice"] = "#{class_name} was succesfully updated" + redirect_to :action => "show#{suffix}/" + @#{singular_name}.id.to_s + else + render "#{singular_name}/edit#{suffix}" + end + end + + private + def render#{suffix}_scaffold(action = caller_method_name(caller)) + if template_exists?("\#{controller_name}/\#{action}") + render_action(action) + else + @scaffold_class = #{class_name} + @scaffold_singular_name, @scaffold_plural_name = "#{singular_name}", "#{plural_name}" + @scaffold_suffix = "#{suffix}" + add_instance_variables_to_assigns + + @content_for_layout = @template.render_file(scaffold_path(action.sub(/#{suffix}$/, "")), false) + self.active_layout ? render_file(self.active_layout, "200 OK", true) : render_file(scaffold_path("layout")) + end + end + + def scaffold_path(template_name) + File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml" + end + + def caller_method_name(caller) + caller.first.scan(/`(.*)'/).first.first # ' ruby-mode + end + end_eval + end + + private + # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through + def validate_options(valid_option_keys, supplied_option_keys) + unknown_option_keys = supplied_option_keys - valid_option_keys + raise(ActionController::ActionControllerError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty? + end + end + end +end diff --git a/actionpack/lib/action_controller/session/active_record_store.rb b/actionpack/lib/action_controller/session/active_record_store.rb new file mode 100644 index 0000000000..c144f62e35 --- /dev/null +++ b/actionpack/lib/action_controller/session/active_record_store.rb @@ -0,0 +1,72 @@ +begin + +require 'active_record' +require 'cgi' +require 'cgi/session' + +# Contributed by Tim Bates +class CGI + class Session + # ActiveRecord database based session storage class. + # + # Implements session storage in a database using the ActiveRecord ORM library. Assumes that the database + # has a table called +sessions+ with columns +id+ (numeric, primary key), +sessid+ and +data+ (text). + # The session data is stored in the +data+ column in YAML format; the user is responsible for ensuring that + # only data that can be YAMLized is stored in the session. + class ActiveRecordStore + # The ActiveRecord class which corresponds to the database table. + class Session < ActiveRecord::Base + serialize :data + # Isn't this class definition beautiful? + end + + # Create a new ActiveRecordStore instance. This constructor is used internally by CGI::Session. + # The user does not generally need to call it directly. + # + # +session+ is the session for which this instance is being created. + # + # +option+ is currently ignored as no options are recognized. + # + # This session's ActiveRecord database row will be created if it does not exist, or opened if it does. + def initialize(session, option=nil) + @session = Session.find_first(["sessid = '%s'", session.session_id]) + if @session + @data = @session.data + else + @session = Session.new("sessid" => session.session_id, "data" => {}) + end + end + + # Update and close the session's ActiveRecord object. + def close + return unless @session + update + @session = nil + end + + # Close and destroy the session's ActiveRecord object. + def delete + return unless @session + @session.destroy + @session = nil + end + + # Restore session state from the session's ActiveRecord object. + def restore + return unless @session + @data = @session.data + end + + # Save session state in the session's ActiveRecord object. + def update + return unless @session + @session.data = @data + @session.save + end + end #ActiveRecordStore + end #Session +end #CGI + +rescue LoadError + # Couldn't load Active Record, so don't make this store available +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/session/drb_server.rb b/actionpack/lib/action_controller/session/drb_server.rb new file mode 100644 index 0000000000..6005b8b2b3 --- /dev/null +++ b/actionpack/lib/action_controller/session/drb_server.rb @@ -0,0 +1,9 @@ +#!/usr/local/bin/ruby -w + +# This is a really simple session storage daemon, basically just a hash, +# which is enabled for DRb access. + +require 'drb' + +DRb.start_service('druby://127.0.0.1:9192', Hash.new) +DRb.thread.join
\ No newline at end of file diff --git a/actionpack/lib/action_controller/session/drb_store.rb b/actionpack/lib/action_controller/session/drb_store.rb new file mode 100644 index 0000000000..8ea23e8fff --- /dev/null +++ b/actionpack/lib/action_controller/session/drb_store.rb @@ -0,0 +1,31 @@ +require 'cgi' +require 'cgi/session' +require 'drb' + +class CGI #:nodoc:all + class Session + class DRbStore + @@session_data = DRbObject.new(nil, 'druby://localhost:9192') + + def initialize(session, option=nil) + @session_id = session.session_id + end + + def restore + @h = @@session_data[@session_id] || {} + end + + def update + @@session_data[@session_id] = @h + end + + def close + update + end + + def delete + @@session_data.delete(@session_id) + end + end + end +end diff --git a/actionpack/lib/action_controller/support/class_attribute_accessors.rb b/actionpack/lib/action_controller/support/class_attribute_accessors.rb new file mode 100644 index 0000000000..786dcf98cb --- /dev/null +++ b/actionpack/lib/action_controller/support/class_attribute_accessors.rb @@ -0,0 +1,57 @@ +# Extends the class object with class and instance accessors for class attributes, +# just like the native attr* accessors for instance attributes. +class Class # :nodoc: + def cattr_reader(*syms) + syms.each do |sym| + class_eval <<-EOS + if ! defined? @@#{sym.id2name} + @@#{sym.id2name} = nil + end + + def self.#{sym.id2name} + @@#{sym} + end + + def #{sym.id2name} + @@#{sym} + end + + def call_#{sym.id2name} + case @@#{sym.id2name} + when Symbol then send(@@#{sym}) + when Proc then @@#{sym}.call(self) + when String then @@#{sym} + else nil + end + end + EOS + end + end + + def cattr_writer(*syms) + syms.each do |sym| + class_eval <<-EOS + if ! defined? @@#{sym.id2name} + @@#{sym.id2name} = nil + end + + def self.#{sym.id2name}=(obj) + @@#{sym.id2name} = obj + end + + def self.set_#{sym.id2name}(obj) + @@#{sym.id2name} = obj + end + + def #{sym.id2name}=(obj) + @@#{sym} = obj + end + EOS + end + end + + def cattr_accessor(*syms) + cattr_reader(*syms) + cattr_writer(*syms) + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/support/class_inheritable_attributes.rb b/actionpack/lib/action_controller/support/class_inheritable_attributes.rb new file mode 100644 index 0000000000..7f061fdf1b --- /dev/null +++ b/actionpack/lib/action_controller/support/class_inheritable_attributes.rb @@ -0,0 +1,37 @@ +# Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of +# their parents' attributes, instead of just a pointer to the same. This means that the child can add elements +# to, for example, an array without those additions being shared with either their parent, siblings, or +# children, which is unlike the regular class-level attributes that are shared across the entire hierarchy. +module ClassInheritableAttributes # :nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + end + + module ClassMethods # :nodoc: + @@classes ||= {} + + def inheritable_attributes + @@classes[self] ||= {} + end + + def write_inheritable_attribute(key, value) + inheritable_attributes[key] = value + end + + def write_inheritable_array(key, elements) + write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil? + write_inheritable_attribute(key, read_inheritable_attribute(key) + elements) + end + + def read_inheritable_attribute(key) + inheritable_attributes[key] + end + + private + def inherited(child) + @@classes[child] = inheritable_attributes.dup + end + + end +end diff --git a/actionpack/lib/action_controller/support/clean_logger.rb b/actionpack/lib/action_controller/support/clean_logger.rb new file mode 100644 index 0000000000..1a36562892 --- /dev/null +++ b/actionpack/lib/action_controller/support/clean_logger.rb @@ -0,0 +1,10 @@ +require 'logger' + +class Logger #:nodoc: + private + remove_const "Format" + Format = "%s\n" + def format_message(severity, timestamp, msg, progname) + Format % [msg] + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/support/cookie_performance_fix.rb b/actionpack/lib/action_controller/support/cookie_performance_fix.rb new file mode 100644 index 0000000000..225cea1905 --- /dev/null +++ b/actionpack/lib/action_controller/support/cookie_performance_fix.rb @@ -0,0 +1,121 @@ +CGI.module_eval { remove_const "Cookie" } + +class CGI #:nodoc: + # This is a cookie class that fixes the performance problems with the default one that ships with 1.8.1 and below. + # It replaces the inheritance on SimpleDelegator with DelegateClass(Array) following the suggestion from Matz on + # http://groups.google.com/groups?th=e3a4e68ba042f842&seekm=c3sioe%241qvm%241%40news.cybercity.dk#link14 + class Cookie < DelegateClass(Array) + # Create a new CGI::Cookie object. + # + # The contents of the cookie can be specified as a +name+ and one + # or more +value+ arguments. Alternatively, the contents can + # be specified as a single hash argument. The possible keywords of + # this hash are as follows: + # + # name:: the name of the cookie. Required. + # value:: the cookie's value or list of values. + # path:: the path for which this cookie applies. Defaults to the + # base directory of the CGI script. + # domain:: the domain for which this cookie applies. + # expires:: the time at which this cookie expires, as a +Time+ object. + # secure:: whether this cookie is a secure cookie or not (default to + # false). Secure cookies are only transmitted to HTTPS + # servers. + # + # These keywords correspond to attributes of the cookie object. + def initialize(name = "", *value) + options = if name.kind_of?(String) + { "name" => name, "value" => value } + else + name + end + unless options.has_key?("name") + raise ArgumentError, "`name' required" + end + + @name = options["name"] + @value = Array(options["value"]) + # simple support for IE + if options["path"] + @path = options["path"] + else + %r|^(.*/)|.match(ENV["SCRIPT_NAME"]) + @path = ($1 or "") + end + @domain = options["domain"] + @expires = options["expires"] + @secure = options["secure"] == true ? true : false + + super(@value) + end + + def __setobj__(obj) + @_dc_obj = obj + end + + attr_accessor("name", "value", "path", "domain", "expires") + attr_reader("secure") + + # Set whether the Cookie is a secure cookie or not. + # + # +val+ must be a boolean. + def secure=(val) + @secure = val if val == true or val == false + @secure + end + + # Convert the Cookie to its string representation. + def to_s + buf = "" + buf += @name + '=' + + if @value.kind_of?(String) + buf += CGI::escape(@value) + else + buf += @value.collect{|v| CGI::escape(v) }.join("&") + end + + if @domain + buf += '; domain=' + @domain + end + + if @path + buf += '; path=' + @path + end + + if @expires + buf += '; expires=' + CGI::rfc1123_date(@expires) + end + + if @secure == true + buf += '; secure' + end + + buf + end + + # Parse a raw cookie string into a hash of cookie-name=>Cookie + # pairs. + # + # cookies = CGI::Cookie::parse("raw_cookie_string") + # # { "name1" => cookie1, "name2" => cookie2, ... } + # + def self.parse(raw_cookie) + cookies = Hash.new([]) + return cookies unless raw_cookie + + raw_cookie.split(/; /).each do |pairs| + name, values = pairs.split('=',2) + next unless name and values + name = CGI::unescape(name) + values ||= "" + values = values.split('&').collect{|v| CGI::unescape(v) } + unless cookies.has_key?(name) + cookies[name] = new({ "name" => name, "value" => values }) + end + end + + cookies + end + end # class Cookie +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/support/inflector.rb b/actionpack/lib/action_controller/support/inflector.rb new file mode 100644 index 0000000000..05ff4fede9 --- /dev/null +++ b/actionpack/lib/action_controller/support/inflector.rb @@ -0,0 +1,78 @@ +# The Inflector transforms words from singular to plural, class names to table names, modulized class names to ones without, +# and class names to foreign keys. +module Inflector + extend self + + def pluralize(word) + result = word.dup + plural_rules.each do |(rule, replacement)| + break if result.gsub!(rule, replacement) + end + return result + end + + def singularize(word) + result = word.dup + singular_rules.each do |(rule, replacement)| + break if result.gsub!(rule, replacement) + end + return result + end + + def camelize(lower_case_and_underscored_word) + lower_case_and_underscored_word.gsub(/(^|_)(.)/){$2.upcase} + end + + def underscore(camel_cased_word) + camel_cased_word.gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase + end + + def demodulize(class_name_in_module) + class_name_in_module.gsub(/^.*::/, '') + end + + def tableize(class_name) + pluralize(underscore(class_name)) + end + + def classify(table_name) + camelize(singularize(table_name)) + end + + def foreign_key(class_name, separate_class_name_and_id_with_underscore = true) + Inflector.underscore(Inflector.demodulize(class_name)) + + (separate_class_name_and_id_with_underscore ? "_id" : "id") + end + + private + def plural_rules #:doc: + [ + [/(x|ch|ss)$/, '\1es'], # search, switch, fix, box, process, address + [/([^aeiouy]|qu)y$/, '\1ies'], # query, ability, agency + [/(?:([^f])fe|([lr])f)$/, '\1\2ves'], # half, safe, wife + [/sis$/, 'ses'], # basis, diagnosis + [/([ti])um$/, '\1a'], # datum, medium + [/person$/, 'people'], # person, salesperson + [/man$/, 'men'], # man, woman, spokesman + [/child$/, 'children'], # child + [/s$/, 's'], # no change (compatibility) + [/$/, 's'] + ] + end + + def singular_rules #:doc: + [ + [/(x|ch|ss)es$/, '\1'], + [/([^aeiouy]|qu)ies$/, '\1y'], + [/([lr])ves$/, '\1f'], + [/([^f])ves$/, '\1fe'], + [/(analy|ba|diagno|parenthe|progno|synop|the)ses$/, '\1sis'], + [/([ti])a$/, '\1um'], + [/people$/, 'person'], + [/men$/, 'man'], + [/status$/, 'status'], + [/children$/, 'child'], + [/s$/, ''] + ] + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/templates/rescues/_request_and_response.rhtml b/actionpack/lib/action_controller/templates/rescues/_request_and_response.rhtml new file mode 100644 index 0000000000..f1b4a2a1dd --- /dev/null +++ b/actionpack/lib/action_controller/templates/rescues/_request_and_response.rhtml @@ -0,0 +1,28 @@ +<% + base_dir = File.expand_path(File.dirname(__FILE__)) + + request_parameters_without_action = @request.parameters.clone + request_parameters_without_action.delete("action") + request_parameters_without_action.delete("controller") + + request_dump = request_parameters_without_action.inspect.gsub(/,/, ",\n") + session_dump = @request.session.instance_variable_get("@data").inspect.gsub(/,/, ",\n") + response_dump = @response.inspect.gsub(/,/, ",\n") + + template_assigns = @response.template.instance_variable_get("@assigns") + %w( response exception template session request template_root template_class url ignore_missing_templates logger cookies headers params ).each { |t| template_assigns.delete(t) } + template_dump = template_assigns.inspect.gsub(/,/, ",\n") +%> + +<h2 style="margin-top: 30px">Request</h2> +<p><b>Parameters</b>: <%=h request_dump == "{}" ? "None" : request_dump %></p> + +<p><a href="#" onclick="document.getElementById('session_dump').style.display='block'; return false;">Show session dump</a></p> +<div id="session_dump" style="display:none"><%= debug(@request.session.instance_variable_get("@data")) %></div> + + +<h2 style="margin-top: 30px">Response</h2> +<b>Headers</b>: <%=h @response.headers.inspect.gsub(/,/, ",\n") %><br/> + +<p><a href="#" onclick="document.getElementById('template_dump').style.display='block'; return false;">Show template parameters</a></p> +<div id="template_dump" style="display:none"><%= debug(template_assigns) %></div> diff --git a/actionpack/lib/action_controller/templates/rescues/diagnostics.rhtml b/actionpack/lib/action_controller/templates/rescues/diagnostics.rhtml new file mode 100644 index 0000000000..4eb1ed0439 --- /dev/null +++ b/actionpack/lib/action_controller/templates/rescues/diagnostics.rhtml @@ -0,0 +1,22 @@ +<% + base_dir = File.expand_path(File.dirname(__FILE__)) + + clean_backtrace = @exception.backtrace.collect { |line| line.gsub(base_dir, "").gsub("/../config/environments/../../", "") } + app_trace = clean_backtrace.reject { |line| line[0..6] == "vendor/" || line.include?("dispatch.cgi") } + framework_trace = clean_backtrace - app_trace +%> + +<h1> + <%=h @exception.class.to_s %> in + <%=h @request.parameters["controller"].capitalize %>#<%=h @request.parameters["action"] %> +</h1> +<p><%=h @exception.message %></p> + +<% unless app_trace.empty? %><pre><code><%=h app_trace.collect { |line| line.gsub("/../", "") }.join("\n") %></code></pre><% end %> + +<% unless framework_trace.empty? %> + <a href="#" onclick="document.getElementById('framework_trace').style.display='block'; return false;">Show framework trace</a> + <pre id="framework_trace" style="display:none"><code><%=h framework_trace.join("\n") %></code></pre> +<% end %> + +<%= render_file(@rescues_path + "/_request_and_response.rhtml", false) %> diff --git a/actionpack/lib/action_controller/templates/rescues/layout.rhtml b/actionpack/lib/action_controller/templates/rescues/layout.rhtml new file mode 100644 index 0000000000..d38f3e67f9 --- /dev/null +++ b/actionpack/lib/action_controller/templates/rescues/layout.rhtml @@ -0,0 +1,29 @@ +<html> +<head> + <title>Action Controller: Exception caught</title> + <style> + body { background-color: #fff; color: #333; } + + body, p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; + } + + pre { + background-color: #eee; + padding: 10px; + font-size: 11px; + } + + a { color: #000; } + a:visited { color: #666; } + a:hover { color: #fff; background-color:#000; } + </style> +</head> +<body> + +<%= @contents %> + +</body> +</html>
\ No newline at end of file diff --git a/actionpack/lib/action_controller/templates/rescues/missing_template.rhtml b/actionpack/lib/action_controller/templates/rescues/missing_template.rhtml new file mode 100644 index 0000000000..dbfdf76947 --- /dev/null +++ b/actionpack/lib/action_controller/templates/rescues/missing_template.rhtml @@ -0,0 +1,2 @@ +<h1>Template is missing</h1> +<p><%=h @exception.message %></p> diff --git a/actionpack/lib/action_controller/templates/rescues/template_error.rhtml b/actionpack/lib/action_controller/templates/rescues/template_error.rhtml new file mode 100644 index 0000000000..326fd0b057 --- /dev/null +++ b/actionpack/lib/action_controller/templates/rescues/template_error.rhtml @@ -0,0 +1,26 @@ +<% + base_dir = File.expand_path(File.dirname(__FILE__)) + + framework_trace = @exception.original_exception.backtrace.collect do |line| + line.gsub(base_dir, "").gsub("/../config/environments/../../", "") + end +%> + +<h1> + <%=h @exception.original_exception.class.to_s %> in + <%=h @request.parameters["controller"].capitalize %>#<%=h @request.parameters["action"] %> +</h1> + +<p> + Showing <i><%=h @exception.file_name %></i> where line <b>#<%=h @exception.line_number %></b> raised + <u><%=h @exception.message %></u> +</p> + +<pre><code><%=h @exception.source_extract %></code></pre> + +<p><%=h @exception.sub_template_message %></p> + +<a href="#" onclick="document.getElementById('framework_trace').style.display='block'">Show template trace</a> +<pre id="framework_trace" style="display:none"><code><%=h framework_trace.join("\n") %></code></pre> + +<%= render_file(@rescues_path + "/_request_and_response.rhtml", false) %> diff --git a/actionpack/lib/action_controller/templates/rescues/unknown_action.rhtml b/actionpack/lib/action_controller/templates/rescues/unknown_action.rhtml new file mode 100644 index 0000000000..683379da10 --- /dev/null +++ b/actionpack/lib/action_controller/templates/rescues/unknown_action.rhtml @@ -0,0 +1,2 @@ +<h1>Unknown action</h1> +<p><%=h @exception.message %></p> diff --git a/actionpack/lib/action_controller/templates/scaffolds/edit.rhtml b/actionpack/lib/action_controller/templates/scaffolds/edit.rhtml new file mode 100644 index 0000000000..1c7f4d9770 --- /dev/null +++ b/actionpack/lib/action_controller/templates/scaffolds/edit.rhtml @@ -0,0 +1,6 @@ +<h1>Editing <%= @scaffold_singular_name %></h1> + +<%= form(@scaffold_singular_name, :action => "../update" + @scaffold_suffix) %> + +<%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}").id %> | +<%= link_to "Back", :action => "list#{@scaffold_suffix}" %> diff --git a/actionpack/lib/action_controller/templates/scaffolds/layout.rhtml b/actionpack/lib/action_controller/templates/scaffolds/layout.rhtml new file mode 100644 index 0000000000..511054abe8 --- /dev/null +++ b/actionpack/lib/action_controller/templates/scaffolds/layout.rhtml @@ -0,0 +1,29 @@ +<html> +<head> + <title>Scaffolding</title> + <style> + body { background-color: #fff; color: #333; } + + body, p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; + } + + pre { + background-color: #eee; + padding: 10px; + font-size: 11px; + } + + a { color: #000; } + a:visited { color: #666; } + a:hover { color: #fff; background-color:#000; } + </style> +</head> +<body> + +<%= @content_for_layout %> + +</body> +</html>
\ No newline at end of file diff --git a/actionpack/lib/action_controller/templates/scaffolds/list.rhtml b/actionpack/lib/action_controller/templates/scaffolds/list.rhtml new file mode 100644 index 0000000000..33af7079b2 --- /dev/null +++ b/actionpack/lib/action_controller/templates/scaffolds/list.rhtml @@ -0,0 +1,24 @@ +<h1>Listing <%= @scaffold_plural_name %></h1> + +<table> + <tr> + <% for column in @scaffold_class.content_columns %> + <th><%= column.human_name %></th> + <% end %> + </tr> + +<% for entry in instance_variable_get("@#{@scaffold_plural_name}") %> + <tr> + <% for column in @scaffold_class.content_columns %> + <td><%= entry.send(column.name) %></td> + <% end %> + <td><%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => entry.id %></td> + <td><%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => entry.id %></td> + <td><%= link_to "Destroy", :action => "destroy#{@scaffold_suffix}", :id => entry.id %></td> + </tr> +<% end %> +</table> + +<br /> + +<%= link_to "New #{@scaffold_singular_name}", :action => "new#{@scaffold_suffix}" %> diff --git a/actionpack/lib/action_controller/templates/scaffolds/new.rhtml b/actionpack/lib/action_controller/templates/scaffolds/new.rhtml new file mode 100644 index 0000000000..02f52e72f5 --- /dev/null +++ b/actionpack/lib/action_controller/templates/scaffolds/new.rhtml @@ -0,0 +1,5 @@ +<h1>New <%= @scaffold_singular_name %></h1> + +<%= form(@scaffold_singular_name, :action => "create" + @scaffold_suffix) %> + +<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>
\ No newline at end of file diff --git a/actionpack/lib/action_controller/templates/scaffolds/show.rhtml b/actionpack/lib/action_controller/templates/scaffolds/show.rhtml new file mode 100644 index 0000000000..10c46342fd --- /dev/null +++ b/actionpack/lib/action_controller/templates/scaffolds/show.rhtml @@ -0,0 +1,9 @@ +<% for column in @scaffold_class.content_columns %> + <p> + <b><%= column.human_name %>:</b> + <%= instance_variable_get("@#{@scaffold_singular_name}").send(column.name) %> + </p> +<% end %> + +<%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}").id %> | +<%= link_to "Back", :action => "list#{@scaffold_suffix}" %> diff --git a/actionpack/lib/action_controller/test_process.rb b/actionpack/lib/action_controller/test_process.rb new file mode 100644 index 0000000000..969f2573c5 --- /dev/null +++ b/actionpack/lib/action_controller/test_process.rb @@ -0,0 +1,195 @@ +require File.dirname(__FILE__) + '/assertions/action_pack_assertions' +require File.dirname(__FILE__) + '/assertions/active_record_assertions' + +module ActionController #:nodoc: + class Base + # Process a test request called with a +TestRequest+ object. + def self.process_test(request) + new.process_test(request) + end + + def process_test(request) #:nodoc: + process(request, TestResponse.new) + end + end + + class TestRequest < AbstractRequest #:nodoc: + attr_writer :cookies + attr_accessor :query_parameters, :request_parameters, :session, :env + attr_accessor :host, :path, :request_uri, :remote_addr + + def initialize(query_parameters = nil, request_parameters = nil, session = nil) + @query_parameters = query_parameters || {} + @request_parameters = request_parameters || {} + @session = session || TestSession.new + + initialize_containers + initialize_default_values + + super() + end + + def reset_session + @session = {} + end + + def cookies + @cookies.freeze + end + + def action=(action_name) + @query_parameters.update({ "action" => action_name }) + @parameters = nil + end + + def request_uri=(uri) + @request_uri = uri + @path = uri.split("?").first + end + + private + def initialize_containers + @env, @cookies = {}, {} + end + + def initialize_default_values + @host = "test.host" + @request_uri = "/" + @remote_addr = "127.0.0.1" + @env["SERVER_PORT"] = 80 + end + end + + class TestResponse < AbstractResponse #:nodoc: + # the class attribute ties a TestResponse to the assertions + class << self + attr_accessor :assertion_target + end + + # initializer + def initialize + TestResponse.assertion_target=self# if TestResponse.assertion_target.nil? + super() + end + + # the response code of the request + def response_code + headers['Status'][0,3].to_i rescue 0 + end + + # was the response successful? + def success? + response_code == 200 + end + + # was the URL not found? + def missing? + response_code == 404 + end + + # were we redirected? + def redirect? + (300..399).include?(response_code) + end + + # was there a server-side error? + def server_error? + (500..599).include?(response_code) + end + + # returns the redirection location or nil + def redirect_url + redirect? ? headers['location'] : nil + end + + # does the redirect location match this regexp pattern? + def redirect_url_match?( pattern ) + return false if redirect_url.nil? + p = Regexp.new(pattern) if pattern.class == String + p = pattern if pattern.class == Regexp + return false if p.nil? + p.match(redirect_url) != nil + end + + # returns the template path of the file which was used to + # render this response (or nil) + def rendered_file(with_controller=false) + unless template.first_render.nil? + unless with_controller + template.first_render + else + template.first_render.split('/').last || template.first_render + end + end + end + + # was this template rendered by a file? + def rendered_with_file? + !rendered_file.nil? + end + + # a shortcut to the flash (or an empty hash if no flash.. hey! that rhymes!) + def flash + session['flash'] || {} + end + + # do we have a flash? + def has_flash? + !session['flash'].nil? + end + + # do we have a flash that has contents? + def has_flash_with_contents? + !flash.empty? + end + + # does the specified flash object exist? + def has_flash_object?(name=nil) + !flash[name].nil? + end + + # does the specified object exist in the session? + def has_session_object?(name=nil) + !session[name].nil? + end + + # a shortcut to the template.assigns + def template_objects + template.assigns || {} + end + + # does the specified template object exist? + def has_template_object?(name=nil) + !template_objects[name].nil? + end +end + + class TestSession #:nodoc: + def initialize(attributes = {}) + @attributes = attributes + end + + def [](key) + @attributes[key] + end + + def []=(key, value) + @attributes[key] = value + end + + def update() end + def close() end + def delete() @attributes = {} end + end +end + +class Test::Unit::TestCase #:nodoc: + private + # execute the request and set/volley the response + def process(action, parameters = nil, session = nil) + @request.action = action.to_s + @request.parameters.update(parameters) unless parameters.nil? + @request.session = ActionController::TestSession.new(session) unless session.nil? + @controller.process(@request, @response) + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/url_rewriter.rb b/actionpack/lib/action_controller/url_rewriter.rb new file mode 100644 index 0000000000..78638da39e --- /dev/null +++ b/actionpack/lib/action_controller/url_rewriter.rb @@ -0,0 +1,170 @@ +module ActionController + # Rewrites urls for Base.redirect_to and Base.url_for in the controller. + class UrlRewriter #:nodoc: + VALID_OPTIONS = [:action, :action_prefix, :action_suffix, :module, :controller, :controller_prefix, :anchor, :params, :path_params, :id, :only_path, :overwrite_params ] + + def initialize(request, controller, action) + @request, @controller, @action = request, controller, action + @rewritten_path = @request.path ? @request.path.dup : "" + end + + def rewrite(options = {}) + validate_options(VALID_OPTIONS, options.keys) + + rewrite_url( + rewrite_path(@rewritten_path, options), + options + ) + end + + def to_s + to_str + end + + def to_str + "#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@controller}, #{@action}, #{@request.parameters.inspect}" + end + + private + def validate_options(valid_option_keys, supplied_option_keys) + unknown_option_keys = supplied_option_keys - valid_option_keys + raise(ActionController::ActionControllerError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty? + end + + def rewrite_url(path, options) + rewritten_url = "" + rewritten_url << @request.protocol unless options[:only_path] + rewritten_url << @request.host_with_port unless options[:only_path] + + rewritten_url << path + rewritten_url << build_query_string(new_parameters(options)) if options[:params] || options[:overwrite_params] + rewritten_url << "##{options[:anchor]}" if options[:anchor] + return rewritten_url + end + + def rewrite_path(path, options) + include_id_in_path_params(options) + + path = rewrite_action(path, options) if options[:action] || options[:action_prefix] + path = rewrite_path_params(path, options) if options[:path_params] + path = rewrite_controller(path, options) if options[:controller] || options[:controller_prefix] + return path + end + + def rewrite_path_params(path, options) + index_action = options[:action] == 'index' || options[:action].nil? && @action == 'index' + id_only = options[:path_params].size == 1 && options[:path_params]['id'] + + if index_action && id_only + path += '/' unless path[-1..-1] == '/' + path += "index/#{options[:path_params]['id']}" + path + else + options[:path_params].inject(path) do |path, pair| + if options[:action].nil? && @request.parameters[pair.first] + path.sub(/\b#{@request.parameters[pair.first]}\b/, pair.last.to_s) + else + path += "/#{pair.last}" + end + end + end + end + + def rewrite_action(path, options) + # This regex assumes that "index" actions won't be included in the URL + all, controller_prefix, action_prefix, action_suffix = + /^\/(.*)#{@controller}\/(.*)#{@action == "index" ? "" : @action}(.*)/.match(path).to_a + + if @action == "index" + if action_prefix == "index" + # we broke the parsing assumption that this would be excluded, so + # don't tell action_name about our little boo-boo + path = path.sub(action_prefix, action_name(options, nil)) + elsif action_prefix && !action_prefix.empty? + path = path.sub(action_prefix, action_name(options, action_prefix)) + else + path = path.sub(%r(#{@controller}/?), @controller + "/" + action_name(options)) # " ruby-mode + end + else + path = path.sub((action_prefix || "") + @action + (action_suffix || ""), action_name(options, action_prefix)) + end + + if options[:controller_prefix] && !options[:controller] + ensure_slash_suffix(options, :controller_prefix) + if controller_prefix + path = path.sub(controller_prefix, options[:controller_prefix]) + else + path = options[:controller_prefix] + path + end + end + + return path + end + + def rewrite_controller(path, options) + all, controller_prefix = /^\/(.*?)#{@controller}/.match(path).to_a + path = "/" + path << controller_name(options, controller_prefix) + path << action_name(options) if options[:action] + path << path_params_in_list(options) if options[:path_params] + return path + end + + def action_name(options, action_prefix = nil, action_suffix = nil) + ensure_slash_suffix(options, :action_prefix) + ensure_slash_prefix(options, :action_suffix) + + prefix = options[:action_prefix] || action_prefix || "" + suffix = options[:action] == "index" ? "" : (options[:action_suffix] || action_suffix || "") + name = (options[:action] == "index" ? "" : options[:action]) || "" + + return prefix + name + suffix + end + + def controller_name(options, controller_prefix) + options[:controller_prefix] = "#{options[:module]}/#{options[:controller_prefix]}" if options[:module] + ensure_slash_suffix(options, :controller_prefix) + controller_name = options[:controller_prefix] || controller_prefix || "" + controller_name << (options[:controller] + "/") if options[:controller] + return controller_name + end + + def path_params_in_list(options) + options[:path_params].inject("") { |path, pair| path += "/#{pair.last}" } + end + + def ensure_slash_suffix(options, key) + options[key] = options[key] + "/" if options[key] && !options[key].empty? && options[key][-1..-1] != "/" + end + + def ensure_slash_prefix(options, key) + options[key] = "/" + options[key] if options[key] && !options[key].empty? && options[key][0..1] != "/" + end + + def include_id_in_path_params(options) + options[:path_params] = (options[:path_params] || {}).merge({"id" => options[:id]}) if options[:id] + end + + def new_parameters(options) + parameters = options[:params] || existing_parameters + parameters.update(options[:overwrite_params]) if options[:overwrite_params] + parameters.reject { |key,value| value.nil? } + end + + def existing_parameters + @request.parameters.reject { |key, value| %w( id action controller).include?(key) } + end + + # Returns a query string with escaped keys and values from the passed hash. If the passed hash contains an "id" it'll + # be added as a path element instead of a regular parameter pair. + def build_query_string(hash) + elements = [] + query_string = "" + + hash.each { |key, value| elements << "#{CGI.escape(key)}=#{CGI.escape(value.to_s)}" } + unless elements.empty? then query_string << ("?" + elements.join("&")) end + + return query_string + end + end +end diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb new file mode 100644 index 0000000000..c39765d436 --- /dev/null +++ b/actionpack/lib/action_view.rb @@ -0,0 +1,49 @@ +#-- +# Copyright (c) 2004 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +begin + require 'rubygems' + require 'builder' +rescue LoadError + # RubyGems is not available, use included Builder + $:.unshift(File.dirname(__FILE__) + "/action_view/vendor") + require 'action_view/vendor/builder' +ensure + # Temporary patch until it's in Builder 1.2.2 + class BlankSlate + class << self + def hide(name) + undef_method name if instance_methods.include?(name) and name !~ /^(__|instance_eval)/ + end + end + end +end + +require 'action_view/base' +require 'action_view/partials' + +ActionView::Base.class_eval do + include ActionView::Partials +end + +ActionView::Base.load_helpers(File.dirname(__FILE__) + "/action_view/helpers/")
\ No newline at end of file diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb new file mode 100644 index 0000000000..84c8040760 --- /dev/null +++ b/actionpack/lib/action_view/base.rb @@ -0,0 +1,264 @@ +require 'erb' + +module ActionView #:nodoc: + class ActionViewError < StandardError #:nodoc: + end + + # Action View templates can be written in two ways. If the template file has a +.rhtml+ extension then it uses a mixture of ERb + # (included in Ruby) and HTML. If the template file has a +.rxml+ extension then Jim Weirich's Builder::XmlMarkup library is used. + # + # = ERb + # + # You trigger ERb by using embeddings such as <% %> and <%= %>. The difference is whether you want output or not. Consider the + # following loop for names: + # + # <b>Names of all the people</b> + # <% for person in @people %> + # Name: <%= person.name %><br/> + # <% end %> + # + # The loop is setup in regular embedding tags (<% %>) and the name is written using the output embedding tag (<%= %>). Note that this + # is not just a usage suggestion. Regular output functions like print or puts won't work with ERb templates. So this would be wrong: + # + # Hi, Mr. <% puts "Frodo" %> + # + # (If you absolutely must write from within a function, you can use the TextHelper#concat) + # + # == Using sub templates + # + # Using sub templates allows you to sidestep tedious replication and extract common display structures in shared templates. The + # classic example is the use of a header and footer (even though the Action Pack-way would be to use Layouts): + # + # <%= render "shared/header" %> + # Something really specific and terrific + # <%= render "shared/footer" %> + # + # As you see, we use the output embeddings for the render methods. The render call itself will just return a string holding the + # result of the rendering. The output embedding writes it to the current template. + # + # But you don't have to restrict yourself to static includes. Templates can share variables amongst themselves by using instance + # variables defined in using the regular embedding tags. Like this: + # + # <% @page_title = "A Wonderful Hello" %> + # <%= render "shared/header" %> + # + # Now the header can pick up on the @page_title variable and use it for outputting a title tag: + # + # <title><%= @page_title %></title> + # + # == Passing local variables to sub templates + # + # You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values: + # + # <%= render "shared/header", { "headline" => "Welcome", "person" => person } %> + # + # These can now be accessed in shared/header with: + # + # Headline: <%= headline %> + # First name: <%= person.first_name %> + # + # == Template caching + # + # The parsing of ERb templates are cached by default, but the reading of them are not. This means that the application by default + # will reflect changes to the templates immediatly. If you'd like to sacrifice that immediacy for the speed gain given by also + # caching the loading of templates (reading from the file systen), you can turn that on with + # <tt>ActionView::Base.cache_template_loading = true</tt>. + # + # == Builder + # + # Builder templates are a more programatic alternative to ERb. They are especially useful for generating XML content. An +XmlMarkup+ object + # named +xml+ is automatically made available to templates with a +.rxml+ extension. + # + # Here are some basic examples: + # + # xml.em("emphasized") # => <em>emphasized</em> + # xml.em { xml.b("emp & bold") } # => <em><b>emph & bold</b></em> + # xml.a("A Link", "href"=>"http://onestepback.org") # => <a href="http://onestepback.org">A Link</a> + # xm.target("name"=>"compile", "option"=>"fast") # => <target option="fast" name="compile"\> + # # NOTE: order of attributes is not specified. + # + # Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following: + # + # xml.div { + # xml.h1(@person.name) + # xml.p(@person.bio) + # } + # + # would produce something like: + # + # <div> + # <h1>David Heinemeier Hansson</h1> + # <p>A product of Danish Design during the Winter of '79...</p> + # </div> + # + # A full-length RSS example actually used on Basecamp: + # + # xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do + # xml.channel do + # xml.title(@feed_title) + # xml.link(@url) + # xml.description "Basecamp: Recent items" + # xml.language "en-us" + # xml.ttl "40" + # + # for item in @recent_items + # xml.item do + # xml.title(item_title(item)) + # xml.description(item_description(item)) if item_description(item) + # xml.pubDate(item_pubDate(item)) + # xml.guid(@person.firm.account.url + @recent_items.url(item)) + # xml.link(@person.firm.account.url + @recent_items.url(item)) + # + # xml.tag!("dc:creator", item.author_name) if item_has_creator?(item) + # end + # end + # end + # end + # + # More builder documentation can be found at http://builder.rubyforge.org. + class Base + include ERB::Util + + attr_reader :first_render + attr_accessor :base_path, :assigns, :template_extension + attr_accessor :controller + + # Turn on to cache the reading of templates from the file system. Doing so means that you have to restart the server + # when changing templates, but that rendering will be faster. + @@cache_template_loading = false + cattr_accessor :cache_template_loading + + @@compiled_erb_templates = {} + @@loaded_templates = {} + + def self.load_helpers(helper_dir)#:nodoc: + Dir.foreach(helper_dir) do |helper_file| + next unless helper_file =~ /_helper.rb$/ + require helper_dir + helper_file + helper_module_name = helper_file.capitalize.gsub(/_([a-z])/) { |m| $1.capitalize }[0..-4] + + class_eval("include ActionView::Helpers::#{helper_module_name}") if Helpers.const_defined?(helper_module_name) + end + end + + def self.controller_delegate(*methods) + methods.flatten.each do |method| + class_eval <<-end_eval + def #{method}(*args, &block) + controller.send(%(#{method}), *args, &block) + end + end_eval + end + end + + def initialize(base_path = nil, assigns_for_first_render = {}, controller = nil)#:nodoc: + @base_path, @assigns = base_path, assigns_for_first_render + @controller = controller + end + + # Renders the template present at <tt>template_path</tt>. If <tt>use_full_path</tt> is set to true, + # it's relative to the template_root, otherwise it's absolute. The hash in <tt>local_assigns</tt> + # is made available as local variables. + def render_file(template_path, use_full_path = true, local_assigns = {}) + @first_render = template_path if @first_render.nil? + + if use_full_path + template_extension = pick_template_extension(template_path) + template_file_name = full_template_path(template_path, template_extension) + else + template_file_name = template_path + template_extension = template_path.split(".").last + end + + template_source = read_template_file(template_file_name) + + begin + render_template(template_extension, template_source, local_assigns) + rescue Exception => e + if TemplateError === e + e.sub_template_of(template_file_name) + raise e + else + raise TemplateError.new(@base_path, template_file_name, @assigns, template_source, e) + end + end + end + + # Renders the template present at <tt>template_path</tt> (relative to the template_root). + # The hash in <tt>local_assigns</tt> is made available as local variables. + def render(template_path, local_assigns = {}) + render_file(template_path, true, local_assigns) + end + + # Renders the +template+ which is given as a string as either rhtml or rxml depending on <tt>template_extension</tt>. + # The hash in <tt>local_assigns</tt> is made available as local variables. + def render_template(template_extension, template, local_assigns = {}) + b = binding + local_assigns.each { |key, value| eval "#{key} = local_assigns[\"#{key}\"]", b } + @assigns.each { |key, value| instance_variable_set "@#{key}", value } + xml = Builder::XmlMarkup.new(:indent => 2) + + send(pick_rendering_method(template_extension), template, binding) + end + + def pick_template_extension(template_path)#:nodoc: + if erb_template_exists?(template_path) + "rhtml" + elsif builder_template_exists?(template_path) + "rxml" + else + raise ActionViewError, "No rhtml or rxml template found for #{template_path}" + end + end + + def pick_rendering_method(template_extension)#:nodoc: + (template_extension == "rxml" ? "rxml" : "rhtml") + "_render" + end + + def erb_template_exists?(template_path)#:nodoc: + template_exists?(template_path, "rhtml") + end + + def builder_template_exists?(template_path)#:nodoc: + template_exists?(template_path, "rxml") + end + + def file_exists?(template_path)#:nodoc: + erb_template_exists?(template_path) || builder_template_exists?(template_path) + end + + # Returns true is the file may be rendered implicitly. + def file_public?(template_path)#:nodoc: + template_path.split("/").last[0,1] != "_" + end + + private + def full_template_path(template_path, extension) + "#{@base_path}/#{template_path}.#{extension}" + end + + def template_exists?(template_path, extension) + FileTest.exists?(full_template_path(template_path, extension)) + end + + def read_template_file(template_path) + unless cache_template_loading && @@loaded_templates[template_path] + @@loaded_templates[template_path] = File.read(template_path) + end + + @@loaded_templates[template_path] + end + + def rhtml_render(template, binding) + @@compiled_erb_templates[template] ||= ERB.new(template) + @@compiled_erb_templates[template].result(binding) + end + + def rxml_render(template, binding) + @controller.headers["Content-Type"] ||= 'text/xml' + eval(template, binding) + end + end +end + +require 'action_view/template_error' diff --git a/actionpack/lib/action_view/helpers/active_record_helper.rb b/actionpack/lib/action_view/helpers/active_record_helper.rb new file mode 100644 index 0000000000..b02b807fe1 --- /dev/null +++ b/actionpack/lib/action_view/helpers/active_record_helper.rb @@ -0,0 +1,171 @@ +require 'cgi' +require File.dirname(__FILE__) + '/form_helper' + +module ActionView + class Base + @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"fieldWithErrors\">#{html_tag}</div>" } + cattr_accessor :field_error_proc + end + + module Helpers + # The Active Record Helper makes it easier to create forms for records kept in instance variables. The most far-reaching is the form + # method that creates a complete form for all the basic content types of the record (not associations or aggregations, though). This + # is a great of making the record quickly available for editing, but likely to prove lacklusters for a complicated real-world form. + # In that case, it's better to use the input method and the specialized form methods in link:classes/ActionView/Helpers/FormHelper.html + module ActiveRecordHelper + # Returns a default input tag for the type of object returned by the method. Example + # (title is a VARCHAR column and holds "Hello World"): + # input("post", "title") => + # <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /> + def input(record_name, method) + InstanceTag.new(record_name, method, self).to_tag + end + + # Returns an entire form with input tags and everything for a specified Active Record object. Example + # (post is a new record that has a title using VARCHAR and a body using TEXT): + # form("post") => + # <form action='create' method='POST'> + # <p> + # <label for="post_title">Title</label><br /> + # <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /> + # </p> + # <p> + # <label for="post_body">Body</label><br /> + # <textarea cols="40" id="post_body" name="post[body]" rows="20" wrap="virtual"> + # Back to the hill and over it again! + # </textarea> + # </p> + # <input type='submit' value='Create' /> + # </form> + # + # It's possible to specialize the form builder by using a different action name and by supplying another + # block renderer. Example (entry is a new record that has a message attribute using VARCHAR): + # + # form("entry", :action => "sign", :input_block => + # Proc.new { |record, column| "#{column.human_name}: #{input(record, column.name)}<br />" }) => + # + # <form action='sign' method='POST'> + # Message: + # <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /><br /> + # <input type='submit' value='Sign' /> + # </form> + def form(record_name, options = {}) + record = instance_eval("@#{record_name}") + action = options[:action] || (record.new_record? ? "create" : "update") + id_field = record.new_record? ? "" : InstanceTag.new(record_name, "id", self).to_input_field_tag("hidden") + + "<form action='#{action}' method='POST'>" + + id_field + all_input_tags(record, record_name, options) + + "<input type='submit' value='#{action.gsub(/[^A-Za-z]/, "").capitalize}' />" + + "</form>" + end + + # Returns a string containing the error message attached to the +method+ on the +object+, if one exists. + # This error message is wrapped in a DIV tag, which can be specialized to include both a +prepend_text+ and +append_text+ + # to properly introduce the error and a +css_class+ to style it accordingly. Examples (post has an error message + # "can't be empty" on the title attribute): + # + # <%= error_message_on "post", "title" %> => + # <div class="formError">can't be empty</div> + # + # <%= error_message_on "post", "title", "Title simply ", " (or it won't work)", "inputError" %> => + # <div class="inputError">Title simply can't be empty (or it won't work)</div> + def error_message_on(object, method, prepend_text = "", append_text = "", css_class = "formError") + if errors = instance_eval("@#{object}").errors.on(method) + "<div class=\"#{css_class}\">#{prepend_text + (errors.is_a?(Array) ? errors.first : errors) + append_text}</div>" + end + end + + def error_messages_for(object_name) + object = instance_eval("@#{object_name}") + unless object.errors.empty? + "<div id=\"errorExplanation\">" + + "<h2>#{object.errors.count} error#{"s" unless object.errors.count == 1} prohibited this #{object_name.gsub("_", " ")} from being saved</h2>" + + "<p>There were problems with the following fields (marked in red below):</p>" + + "<ul>#{object.errors.full_messages.collect { |msg| "<li>#{msg}</li>"}}</ul>" + + "</div>" + end + end + + private + def all_input_tags(record, record_name, options) + input_block = options[:input_block] || default_input_block + record.class.content_columns.collect{ |column| input_block.call(record_name, column) }.join("\n") + end + + def default_input_block + Proc.new { |record, column| "<p><label for=\"#{record}_#{column.name}\">#{column.human_name}</label><br />#{input(record, column.name)}</p>" } + end + end + + class InstanceTag #:nodoc: + def to_tag(options = {}) + case column_type + when :string + field_type = @method_name.include?("password") ? "password" : "text" + to_input_field_tag(field_type, options) + when :text + to_text_area_tag(options) + when :integer, :float + to_input_field_tag("text", options) + when :date + to_date_select_tag(options) + when :datetime + to_datetime_select_tag(options) + when :boolean + to_boolean_select_tag(options) + end + end + + alias_method :tag_without_error_wrapping, :tag + + def tag(name, options) + if object.respond_to?("errors") && object.errors.respond_to?("on") + error_wrapping(tag_without_error_wrapping(name, options), object.errors.on(@method_name)) + else + tag_without_error_wrapping(name, options) + end + end + + alias_method :content_tag_without_error_wrapping, :content_tag + + def content_tag(name, value, options) + if object.respond_to?("errors") && object.errors.respond_to?("on") + error_wrapping(content_tag_without_error_wrapping(name, value, options), object.errors.on(@method_name)) + else + content_tag_without_error_wrapping(name, value, options) + end + end + + alias_method :to_date_select_tag_without_error_wrapping, :to_date_select_tag + def to_date_select_tag(options = {}) + if object.respond_to?("errors") && object.errors.respond_to?("on") + error_wrapping(to_date_select_tag_without_error_wrapping(options), object.errors.on(@method_name)) + else + to_date_select_tag_without_error_wrapping(options) + end + end + + alias_method :to_datetime_select_tag_without_error_wrapping, :to_datetime_select_tag + def to_datetime_select_tag(options = {}) + if object.respond_to?("errors") && object.errors.respond_to?("on") + error_wrapping(to_datetime_select_tag_without_error_wrapping(options), object.errors.on(@method_name)) + else + to_datetime_select_tag_without_error_wrapping(options) + end + end + + def error_wrapping(html_tag, has_error) + has_error ? Base.field_error_proc.call(html_tag, self) : html_tag + end + + def error_message + object.errors.on(@method_name) + end + + def column_type + object.send("column_for_attribute", @method_name).type + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb new file mode 100755 index 0000000000..5526c3eef4 --- /dev/null +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -0,0 +1,230 @@ +require "date" + +module ActionView + module Helpers + # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the select-type methods + # share a number of common options that are as follows: + # + # * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday" would give + # birthday[month] instead of date[month] if passed to the select_month method. + # * <tt>:include_blank</tt> - set to true if it should be possible to set an empty date. + # * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true, the select_month + # method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead of "date[month]". + module DateHelper + DEFAULT_PREFIX = "date" unless const_defined?("DEFAULT_PREFIX") + + # Reports the approximate distance in time between to Time objects. For example, if the distance is 47 minutes, it'll return + # "about 1 hour". See the source for the complete wording list. + def distance_of_time_in_words(from_time, to_time) + distance_in_minutes = ((to_time - from_time) / 60).round + + case distance_in_minutes + when 0 then "less than a minute" + when 1 then "1 minute" + when 2..45 then "#{distance_in_minutes} minutes" + when 46..90 then "about 1 hour" + when 90..1440 then "about #{(distance_in_minutes.to_f / 60.0).round} hours" + when 1441..2880 then "1 day" + else "#{(distance_in_minutes / 1440).round} days" + end + end + + # Like distance_of_time_in_words, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>. + def distance_of_time_in_words_to_now(from_time) + distance_of_time_in_words(from_time, Time.now) + end + + # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute (identified by + # +method+) on an object assigned to the template (identified by +object+). It's possible to tailor the selects through the +options+ hash, + # which both accepts all the keys that each of the individual select builders does (like :use_month_numbers for select_month) and a range + # of discard options. The discard options are <tt>:discard_month</tt> and <tt>:discard_day</tt>. Set to true, they'll drop the respective + # select. Discarding the month select will also automatically discard the day select. + # + # NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed. Additionally, you can get the + # month select before the year by setting :month_before_year to true in the options. This is especially useful for credit card forms. + # Examples: + # + # date_select("post", "written_on") + # date_select("post", "written_on", :start_year => 1995) + # date_select("post", "written_on", :start_year => 1995, :use_month_numbers => true, + # :discard_day => true, :include_blank => true) + # + # The selects are prepared for multi-parameter assignment to an Active Record object. + def date_select(object, method, options = {}) + InstanceTag.new(object, method, self).to_date_select_tag(options) + end + + # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based + # attribute (identified by +method+) on an object assigned to the template (identified by +object+). Examples: + # + # datetime_select("post", "written_on") + # datetime_select("post", "written_on", :start_year => 1995) + # + # The selects are prepared for multi-parameter assignment to an Active Record object. + def datetime_select(object, method, options = {}) + InstanceTag.new(object, method, self).to_datetime_select_tag(options) + end + + # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+. + def select_date(date = Date.today, options = {}) + select_year(date, options) + select_month(date, options) + select_day(date, options) + end + + # Returns a set of html select-tags (one for year, month, day, hour, and minute) preselected the +datetime+. + def select_datetime(datetime = Time.now, options = {}) + select_year(datetime, options) + select_month(datetime, options) + select_day(datetime, options) + + select_hour(datetime, options) + select_minute(datetime, options) + end + + # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected. + # The <tt>minute</tt> can also be substituted for a minute number. + def select_minute(datetime, options = {}) + minute_options = [] + + 0.upto(59) do |minute| + minute_options << ((datetime.kind_of?(Fixnum) ? datetime : datetime.min) == minute ? + "<option selected=\"selected\">#{leading_zero_on_single_digits(minute)}</option>\n" : + "<option>#{leading_zero_on_single_digits(minute)}</option>\n" + ) + end + + select_html("minute", minute_options, options[:prefix], options[:include_blank], options[:discard_type]) + end + + # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. + # The <tt>hour</tt> can also be substituted for a hour number. + def select_hour(datetime, options = {}) + hour_options = [] + + 0.upto(23) do |hour| + hour_options << ((datetime.kind_of?(Fixnum) ? datetime : datetime.hour) == hour ? + "<option selected=\"selected\">#{leading_zero_on_single_digits(hour)}</option>\n" : + "<option>#{leading_zero_on_single_digits(hour)}</option>\n" + ) + end + + select_html("hour", hour_options, options[:prefix], options[:include_blank], options[:discard_type]) + end + + # Returns a select tag with options for each of the days 1 through 31 with the current day selected. + # The <tt>date</tt> can also be substituted for a hour number. + def select_day(date, options = {}) + day_options = [] + + 1.upto(31) do |day| + day_options << ((date.kind_of?(Fixnum) ? date : date.day) == day ? + "<option selected=\"selected\">#{day}</option>\n" : + "<option>#{day}</option>\n" + ) + end + + select_html("day", day_options, options[:prefix], options[:include_blank], options[:discard_type]) + end + + # Returns a select tag with options for each of the months January through December with the current month selected. + # The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are used as values + # (what's submitted to the server). It's also possible to use month numbers for the presentation instead of names -- + # set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you want both numbers and names, + # set the <tt>:add_month_numbers</tt> key in +options+ to true. Examples: + # + # select_month(Date.today) # Will use keys like "January", "March" + # select_month(Date.today, :use_month_numbers => true) # Will use keys like "1", "3" + # select_month(Date.today, :add_month_numbers => true) # Will use keys like "1 - January", "3 - March" + def select_month(date, options = {}) + month_options = [] + + 1.upto(12) do |month_number| + month_name = if options[:use_month_numbers] + month_number + elsif options[:add_month_numbers] + month_number.to_s + " - " + Date::MONTHNAMES[month_number] + else + Date::MONTHNAMES[month_number] + end + + month_options << ((date.kind_of?(Fixnum) ? date : date.month) == month_number ? + "<option value='#{month_number}' selected=\"selected\">#{month_name}</option>\n" : + "<option value='#{month_number}'>#{month_name}</option>\n" + ) + end + + select_html("month", month_options, options[:prefix], options[:include_blank], options[:discard_type]) + end + + # Returns a select tag with options for each of the five years on each side of the current, which is selected. The five year radius + # can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the +options+. The <tt>date</tt> can also be substituted + # for a year given as a number. Example: + # + # select_year(Date.today, :start_year => 1992, :end_year => 2007) + def select_year(date, options = {}) + year_options = [] + unless date.kind_of?(Fixnum) then default_start_year, default_end_year = date.year - 5, date.year + 5 end + + (options[:start_year] || default_start_year).upto(options[:end_year] || default_end_year) do |year| + year_options << ((date.kind_of?(Fixnum) ? date : date.year) == year ? + "<option selected=\"selected\">#{year}</option>\n" : + "<option>#{year}</option>\n" + ) + end + + select_html("year", year_options, options[:prefix], options[:include_blank], options[:discard_type]) + end + + private + def select_html(type, options, prefix = nil, include_blank = false, discard_type = false) + select_html = "<select name='#{prefix || DEFAULT_PREFIX}" + select_html << "[#{type}]" unless discard_type + select_html << "'>\n" + select_html << "<option></option>\n" if include_blank + select_html << options.to_s + select_html << "</select>\n" + + return select_html + end + + def leading_zero_on_single_digits(number) + number > 9 ? number : "0#{number}" + end + end + + class InstanceTag #:nodoc: + include DateHelper + + def to_date_select_tag(options = {}) + defaults = { :discard_type => true } + options = defaults.merge(options) + options_with_prefix = Proc.new { |position| options.update({ :prefix => "#{@object_name}[#{@method_name}(#{position}i)]" }) } + date = options[:include_blank] ? (value || 0) : (value || Date.today) + + date_select = "" + + if options[:month_before_year] + date_select << select_month(date, options_with_prefix.call(2)) unless options[:discard_month] + date_select << select_year(date, options_with_prefix.call(1)) + else + date_select << select_year(date, options_with_prefix.call(1)) + date_select << select_month(date, options_with_prefix.call(2)) unless options[:discard_month] + end + + date_select << select_day(date, options_with_prefix.call(3)) unless options[:discard_day] || options[:discard_month] + + return date_select + end + + def to_datetime_select_tag(options = {}) + defaults = { :discard_type => true } + options = defaults.merge(options) + options_with_prefix = Proc.new { |position| options.update({ :prefix => "#{@object_name}[#{@method_name}(#{position}i)]" }) } + datetime = options[:include_blank] ? (value || 0) : (value || Time.now) + + datetime_select = select_year(datetime, options_with_prefix.call(1)) + datetime_select << select_month(datetime, options_with_prefix.call(2)) unless options[:discard_month] + datetime_select << select_day(datetime, options_with_prefix.call(3)) unless options[:discard_day] || options[:discard_month] + datetime_select << " — " + select_hour(datetime, options_with_prefix.call(4)) unless options[:discard_hour] + datetime_select << " : " + select_minute(datetime, options_with_prefix.call(5)) unless options[:discard_minute] || options[:discard_hour] + + return datetime_select + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/debug_helper.rb b/actionpack/lib/action_view/helpers/debug_helper.rb new file mode 100644 index 0000000000..8baea6f450 --- /dev/null +++ b/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 <pre>-tag set with the +object+ dumped by YAML. Very readable way to inspect an object. + def debug(object) + begin + Marshal::dump(object) + "<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", " ")}</pre>" + rescue Object => e + # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback + "<code class='debug_dump'>#{h(object.inspect)}</code>" + end + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb new file mode 100644 index 0000000000..389aa302a9 --- /dev/null +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -0,0 +1,182 @@ +require 'cgi' +require File.dirname(__FILE__) + '/date_helper' +require File.dirname(__FILE__) + '/tag_helper' + +module ActionView + module Helpers + # Provides a set of methods for working with forms and especially forms related to objects assigned to the template. + # The following is an example of a complete form for a person object that works for both creates and updates built + # with all the form helpers. The <tt>@person</tt> object was assigned by an action on the controller: + # <form action="save_person" method="post"> + # Name: + # <%= text_field "person", "name", "size" => 20 %> + # + # Password: + # <%= password_field "person", "password", "maxsize" => 20 %> + # + # Single?: + # <%= check_box "person", "single" %> + # + # Description: + # <%= text_area "person", "description", "cols" => 20 %> + # + # <input type="submit" value="Save"> + # </form> + # + # ...is compiled to: + # + # <form action="save_person" method="post"> + # Name: + # <input type="text" id="person_name" name="person[name]" + # size="20" value="<%= @person.name %>" /> + # + # Password: + # <input type="password" id="person_password" name="person[password]" + # size="20" maxsize="20" value="<%= @person.password %>" /> + # + # Single?: + # <input type="checkbox" id="person_single" name="person[single] value="1" /> + # + # Description: + # <textarea cols="20" rows="40" id="person_description" name="person[description]"> + # <%= @person.description %> + # </textarea> + # + # <input type="submit" value="Save"> + # </form> + # + # There's also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html, + # link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html + module FormHelper + # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. + # + # Examples (call, result): + # text_field("post", "title", "size" => 20) + # <input type="text" id="post_title" name="post[title]" size="20" value="#{@post.title}" /> + def text_field(object, method, options = {}) + InstanceTag.new(object, method, self).to_input_field_tag("text", options) + end + + # Works just like text_field, but returns a input tag of the "password" type instead. + def password_field(object, method, options = {}) + InstanceTag.new(object, method, self).to_input_field_tag("password", options) + end + + # Works just like text_field, but returns a input tag of the "hidden" type instead. + def hidden_field(object, method, options = {}) + InstanceTag.new(object, method, self).to_input_field_tag("hidden", options) + end + + # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+) + # on an object assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. + # + # Example (call, result): + # text_area("post", "body", "cols" => 20, "rows" => 40) + # <textarea cols="20" rows="40" id="post_body" name="post[body]"> + # #{@post.body} + # </textarea> + def text_area(object, method, options = {}) + InstanceTag.new(object, method, self).to_text_area_tag(options) + end + + # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). It's intended that +method+ returns an integer and if that + # integer is above zero, then the checkbox is checked. Additional options on the input tag can be passed as a + # hash with +options+. The +checked_value+ defaults to 1 while the default +unchecked_value+ + # is set to 0 which is convenient for boolean values. Usually unchecked checkboxes don't post anything. + # We work around this problem by adding a hidden value with the same name as the checkbox. + # + # Example (call, result). Imagine that @post.validated? returns 1: + # check_box("post", "validated") + # <input type="checkbox" id="post_validate" name="post[validated] value="1" checked="checked" /><input name="post[validated]" type="hidden" value="0" /> + # + # Example (call, result). Imagine that @puppy.gooddog returns no: + # check_box("puppy", "gooddog", {}, "yes", "no") + # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog] value="yes" /><input name="puppy[gooddog]" type="hidden" value="no" /> + def check_box(object, method, options = {}, checked_value = "1", unchecked_value = "0") + InstanceTag.new(object, method, self).to_check_box_tag(options, checked_value, unchecked_value) + end + end + + class InstanceTag #:nodoc: + include Helpers::TagHelper + + attr_reader :method_name, :object_name + + DEFAULT_FIELD_OPTIONS = { "size" => 30 } unless const_defined?("DEFAULT_FIELD_OPTIONS") + DEFAULT_TEXT_AREA_OPTIONS = { "wrap" => "virtual", "cols" => 40, "rows" => 20 } unless const_defined?("DEFAULT_TEXT_AREA_OPTIONS") + + def initialize(object_name, method_name, template_object, local_binding = nil) + @object_name, @method_name = object_name, method_name + @template_object, @local_binding = template_object, local_binding + end + + def to_input_field_tag(field_type, options = {}) + html_options = DEFAULT_FIELD_OPTIONS.merge(options) + html_options.merge!({ "size" => options["maxlength"]}) if options["maxlength"] && !options["size"] + html_options.merge!({ "type" => field_type, "value" => value.to_s }) + add_default_name_and_id(html_options) + tag("input", html_options) + end + + def to_text_area_tag(options = {}) + options = DEFAULT_TEXT_AREA_OPTIONS.merge(options) + add_default_name_and_id(options) + content_tag("textarea", html_escape(value), options) + end + + def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0") + options.merge!({"checked" => "checked"}) if !value.nil? && ((value.is_a?(TrueClass) || value.is_a?(FalseClass)) ? value : value.to_i > 0) + options.merge!({ "type" => "checkbox", "value" => checked_value }) + add_default_name_and_id(options) + tag("input", options) << tag("input", ({ "name" => options['name'], "type" => "hidden", "value" => unchecked_value })) + end + + def to_date_tag() + defaults = { "discard_type" => true } + date = value || Date.today + options = Proc.new { |position| defaults.update({ :prefix => "#{@object_name}[#{@method_name}(#{position}i)]" }) } + + html_day_select(date, options.call(3)) + + html_month_select(date, options.call(2)) + + html_year_select(date, options.call(1)) + end + + def to_boolean_select_tag(options = {}) + add_default_name_and_id(options) + tag_text = "<select" + tag_text << tag_options(options) + tag_text << "><option value=\"false\"" + tag_text << " selected" if value == false + tag_text << ">False</option><option value=\"true\"" + tag_text << " selected" if value + tag_text << ">True</option></select>" + end + + def object + @template_object.instance_variable_get "@#{@object_name}" + end + + def value + object.send(@method_name) unless object.nil? + end + + private + def add_default_name_and_id(options) + options['name'] = tag_name unless options.has_key? "name" + options['id'] = tag_id unless options.has_key? "id" + end + + def tag_name + "#{@object_name}[#{@method_name}]" + end + + def tag_id + "#{@object_name}_#{@method_name}" + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb new file mode 100644 index 0000000000..ca3798ede6 --- /dev/null +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -0,0 +1,212 @@ +require 'cgi' +require 'erb' +require File.dirname(__FILE__) + '/form_helper' + +module ActionView + module Helpers + # Provides a number of methods for turning different kinds of containers into a set of option tags. Neither of the methods provide + # the actual select tag, so you'll need to construct that in HTML manually. + module FormOptionsHelper + include ERB::Util + + def select(object, method, choices, options = {}, html_options = {}) + InstanceTag.new(object, method, self).to_select_tag(choices, options, html_options) + end + + def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {}) + InstanceTag.new(object, method, self).to_collection_select_tag(collection, value_method, text_method, options, html_options) + end + + def country_select(object, method, priority_countries = nil, options = {}, html_options = {}) + InstanceTag.new(object, method, self).to_country_select_tag(priority_countries, options, html_options) + end + + # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container + # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and + # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values + # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +Selected+ + # may also be an array of values to be selected when using a multiple select. + # + # Examples (call, result): + # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]]) + # <option value="$">Dollar</option>\n<option value="DKK">Kroner</option> + # + # options_for_select([ "VISA", "Mastercard" ], "Mastercard") + # <option>VISA</option>\n<option selected="selected">Mastercard</option> + # + # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40") + # <option value="$20">Basic</option>\n<option value="$40" selected="selected">Plus</option> + # + # options_for_select([ "VISA", "Mastercard", "Discover" ], ["VISA", "Discover"]) + # <option selected="selected">VISA</option>\n<option>Mastercard</option>\n<option selected="selected">Discover</option> + def options_for_select(container, selected = nil) + container = container.to_a if Hash === container + + options_for_select = container.inject([]) do |options, element| + if element.respond_to?(:first) && element.respond_to?(:last) + is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element.last) : element.last == selected) ) + if is_selected + options << "<option value=\"#{html_escape(element.last.to_s)}\" selected=\"selected\">#{html_escape(element.first.to_s)}</option>" + else + options << "<option value=\"#{html_escape(element.last.to_s)}\">#{html_escape(element.first.to_s)}</option>" + end + else + is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element) : element == selected) ) + options << ((is_selected) ? "<option selected=\"selected\">#{html_escape(element.to_s)}</option>" : "<option>#{html_escape(element.to_s)}</option>") + end + end + + options_for_select.join("\n") + end + + # Returns a string of option tags that has been compiled by iterating over the +collection+ and assigning the + # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text. + # If +selected_value+ is specified, the element returning a match on +value_method+ will get the selected option tag. + # + # Example (call, result). Imagine a loop iterating over each +person+ in <tt>@project.people</tt> to generate a input tag: + # options_from_collection_for_select(@project.people, "id", "name") + # <option value="#{person.id}">#{person.name}</option> + def options_from_collection_for_select(collection, value_method, text_method, selected_value = nil) + options_for_select( + collection.inject([]) { |options, object| options << [ object.send(text_method), object.send(value_method) ] }, + selected_value + ) + end + + # Returns a string of option tags, like options_from_collection_for_select, but surrounds them by <optgroup> tags. + # + # An array of group objects are passed. Each group should return an array of options when calling group_method + # Each group should should return its name when calling group_label_method. + # + # html_option_groups_from_collection(@continents, "countries", "contient_name", "country_id", "country_name", @selected_country.id) + # + # Could become: + # <optgroup label="Africa"> + # <select>Egypt</select> + # <select>Rwanda</select> + # ... + # </optgroup> + # <optgroup label="Asia"> + # <select>China</select> + # <select>India</select> + # <select>Japan</select> + # ... + # </optgroup> + # + # with objects of the following classes: + # class Continent + # def initialize(p_name, p_countries) @continent_name = p_name; @countries = p_countries; end + # def continent_name() @continent_name; end + # def countries() @countries; end + # end + # class Country + # def initialize(id, name) @id = id; @name = name end + # def country_id() @id; end + # def country_name() @name; end + # end + def option_groups_from_collection_for_select(collection, group_method, group_label_method, + option_key_method, option_value_method, selected_key = nil) + collection.inject("") do |options_for_select, group| + group_label_string = eval("group.#{group_label_method}") + options_for_select += "<optgroup label=\"#{html_escape(group_label_string)}\">" + options_for_select += options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key) + options_for_select += '</optgroup>' + end + end + + # Returns a string of option tags for pretty much any country in the world. Supply a country name as +selected+ to + # have it marked as the selected option tag. You can also supply an array of countries as +priority_countries+, so + # that they will be listed above the rest of the (long) list. + def country_options_for_select(selected = nil, priority_countries = nil) + country_options = "" + + if priority_countries + country_options += options_for_select(priority_countries, selected) + country_options += "<option>-------------</option>\n" + end + + if priority_countries && priority_countries.include?(selected) + country_options += options_for_select(COUNTRIES - priority_countries, selected) + else + country_options += options_for_select(COUNTRIES, selected) + end + + return country_options + end + + + private + # All the countries included in the country_options output. + COUNTRIES = [ "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", + "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", + "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", + "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", + "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory", + "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burma", "Burundi", "Cambodia", + "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", + "Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia", + "Comoros", "Congo", "Congo, the Democratic Republic of the", "Cook Islands", + "Costa Rica", "Cote d'Ivoire", "Croatia", "Cyprus", "Czech Republic", "Denmark", + "Djibouti", "Dominica", "Dominican Republic", "East Timor", "Ecuador", "Egypt", + "El Salvador", "England", "Equatorial Guinea", "Eritrea", "Espana", "Estonia", + "Ethiopia", "Falkland Islands", "Faroe Islands", "Fiji", "Finland", "France", + "French Guiana", "French Polynesia", "French Southern Territories", "Gabon", "Gambia", + "Georgia", "Germany", "Ghana", "Gibraltar", "Great Britain", "Greece", "Greenland", + "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana", + "Haiti", "Heard and Mc Donald Islands", "Honduras", "Hong Kong", "Hungary", "Iceland", + "India", "Indonesia", "Ireland", "Israel", "Italy", "Jamaica", "Japan", "Jordan", + "Kazakhstan", "Kenya", "Kiribati", "Korea, Republic of", "Korea (South)", "Kuwait", + "Kyrgyzstan", "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", + "Liberia", "Liechtenstein", "Lithuania", "Luxembourg", "Macau", "Macedonia", + "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", + "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", + "Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia", + "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", + "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", + "Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Ireland", + "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Panama", + "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland", + "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russia", "Rwanda", + "Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", + "Samoa (Independent)", "San Marino", "Sao Tome and Principe", "Saudi Arabia", + "Scotland", "Senegal", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", + "Slovenia", "Solomon Islands", "Somalia", "South Africa", + "South Georgia and the South Sandwich Islands", "South Korea", "Spain", "Sri Lanka", + "St. Helena", "St. Pierre and Miquelon", "Suriname", "Svalbard and Jan Mayen Islands", + "Swaziland", "Sweden", "Switzerland", "Taiwan", "Tajikistan", "Tanzania", "Thailand", + "Togo", "Tokelau", "Tonga", "Trinidad", "Trinidad and Tobago", "Tunisia", "Turkey", + "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", + "United Arab Emirates", "United Kingdom", "United States", + "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", + "Vatican City State (Holy See)", "Venezuela", "Viet Nam", "Virgin Islands (British)", + "Virgin Islands (U.S.)", "Wales", "Wallis and Futuna Islands", "Western Sahara", + "Yemen", "Zambia", "Zimbabwe" ] unless const_defined?("COUNTRIES") + end + + class InstanceTag #:nodoc: + include FormOptionsHelper + + def to_select_tag(choices, options, html_options) + add_default_name_and_id(html_options) + content_tag("select", add_blank_option(options_for_select(choices, value), options[:include_blank]), html_options) + end + + def to_collection_select_tag(collection, value_method, text_method, options, html_options) + add_default_name_and_id(html_options) + content_tag( + "select", add_blank_option(options_from_collection_for_select(collection, value_method, text_method, value), options[:include_blank]), html_options + ) + end + + def to_country_select_tag(priority_countries, options, html_options) + add_default_name_and_id(html_options) + content_tag("select", add_blank_option(country_options_for_select(value, priority_countries), options[:include_blank]), html_options) + end + + private + def add_blank_option(option_tags, add_blank) + add_blank ? "<option></option>\n" + option_tags : option_tags + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb new file mode 100644 index 0000000000..90084c7a8d --- /dev/null +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -0,0 +1,59 @@ +require 'cgi' + +module ActionView + module Helpers + # This is poor man's Builder for the rare cases where you need to programmatically make tags but can't use Builder. + module TagHelper + include ERB::Util + + # Examples: + # * tag("br") => <br /> + # * tag("input", { "type" => "text"}) => <input type="text" /> + def tag(name, options = {}, open = false) + "<#{name + tag_options(options)}" + (open ? ">" : " />") + end + + # Examples: + # * content_tag("p", "Hello world!") => <p>Hello world!</p> + # * content_tag("div", content_tag("p", "Hello world!"), "class" => "strong") => + # <div class="strong"><p>Hello world!</p></div> + def content_tag(name, content, options = {}) + "<#{name + tag_options(options)}>#{content}</#{name}>" + end + + # Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like + # ActionController::Base#url_for. + def form_tag(url_for_options, options = {}, *parameters_for_url) + html_options = { "method" => "POST" }.merge(options) + + if html_options[:multipart] + html_options["enctype"] = "multipart/form-data" + html_options.delete(:multipart) + end + + html_options["action"] = url_for(url_for_options, *parameters_for_url) + + tag("form", html_options, true) + end + + alias_method :start_form_tag, :form_tag + + # Outputs "</form>" + def end_form_tag + "</form>" + end + + + private + def tag_options(options) + if options.empty? + "" + else + " " + options.collect { |pair| + "#{pair.first}=\"#{html_escape(pair.last)}\"" + }.sort.join(" ") + end + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb new file mode 100644 index 0000000000..7e05e468b8 --- /dev/null +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -0,0 +1,111 @@ +module ActionView + module Helpers #:nodoc: + # Provides a set of methods for working with text strings that can help unburden the level of inline Ruby code in the + # templates. In the example below we iterate over a collection of posts provided to the template and prints each title + # after making sure it doesn't run longer than 20 characters: + # <% for post in @posts %> + # Title: <%= truncate(post.title, 20) %> + # <% end %> + module TextHelper + # The regular puts and print are outlawed in eRuby. It's recommended to use the <%= "hello" %> form instead of print "hello". + # If you absolutely must use a method-based output, you can use concat. It's use like this <% concat "hello", binding %>. Notice that + # it doesn't have an equal sign in front. Using <%= concat "hello" %> would result in a double hello. + def concat(string, binding) + eval("_erbout", binding).concat(string) + end + + # Truncates +text+ to the length of +length+ and replaces the last three characters with the +truncate_string+ + # if the +text+ is longer than +length+. + def truncate(text, length = 30, truncate_string = "...") + if text.nil? then return end + if text.length > length then text[0..(length - 3)] + truncate_string else text end + end + + # Highlights the +phrase+ where it is found in the +text+ by surrounding it like + # <strong class="highlight">I'm a highlight phrase</strong>. The highlighter can be specialized by + # passing +highlighter+ as single-quoted string with \1 where the phrase is supposed to be inserted. + # N.B.: The +phrase+ is sanitized to include only letters, digits, and spaces before use. + def highlight(text, phrase, highlighter = '<strong class="highlight">\1</strong>') + if text.nil? || phrase.nil? then return end + text.gsub(/(#{escape_regexp(phrase)})/i, highlighter) unless text.nil? + end + + # Extracts an excerpt from the +text+ surrounding the +phrase+ with a number of characters on each side determined + # by +radius+. If the phrase isn't found, nil is returned. Ex: + # excerpt("hello my world", "my", 3) => "...lo my wo..." + def excerpt(text, phrase, radius = 100, excerpt_string = "...") + if text.nil? || phrase.nil? then return end + phrase = escape_regexp(phrase) + + if found_pos = text =~ /(#{phrase})/i + start_pos = [ found_pos - radius, 0 ].max + end_pos = [ found_pos + phrase.length + radius, text.length ].min + + prefix = start_pos > 0 ? excerpt_string : "" + postfix = end_pos < text.length ? excerpt_string : "" + + prefix + text[start_pos..end_pos].strip + postfix + else + nil + end + end + + # Attempts to pluralize the +singular+ word unless +count+ is 1. See source for pluralization rules. + def pluralize(count, singular, plural = nil) + "#{count} " + if count == 1 + singular + elsif plural + plural + elsif Object.const_defined?("Inflector") + Inflector.pluralize(singular) + else + singular + "s" + end + end + + begin + require "redcloth" + + # Returns the text with all the Textile codes turned into HTML-tags. + # <i>This method is only available if RedCloth can be required</i>. + def textilize(text) + RedCloth.new(text).to_html + end + + # Returns the text with all the Textile codes turned into HTML-tags, but without the regular bounding <p> tag. + # <i>This method is only available if RedCloth can be required</i>. + def textilize_without_paragraph(text) + textiled = textilize(text) + if textiled[0..2] == "<p>" then textiled = textiled[3..-1] end + if textiled[-4..-1] == "</p>" then textiled = textiled[0..-5] end + return textiled + end + rescue LoadError + # We can't really help what's not there + end + + begin + require "bluecloth" + + # Returns the text with all the Markdown codes turned into HTML-tags. + # <i>This method is only available if BlueCloth can be required</i>. + def markdown(text) + BlueCloth.new(text).to_html + end + rescue LoadError + # We can't really help what's not there + end + + # Turns all links into words, like "<a href="something">else</a>" to "else". + def strip_links(text) + text.gsub(/<a.*>(.*)<\/a>/m, '\1') + end + + private + # Returns a version of the text that's safe to use in a regular expression without triggering engine features. + def escape_regexp(text) + text.gsub(/([\\|?+*\/\)\(])/) { |m| "\\#{$1}" } + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb new file mode 100644 index 0000000000..feda33d7c1 --- /dev/null +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -0,0 +1,78 @@ +module ActionView + module Helpers + # Provides a set of methods for making easy links and getting urls that depend on the controller and action. This means that + # you can use the same format for links in the views that you do in the controller. The different methods are even named + # synchronously, so link_to uses that same url as is generated by url_for, which again is the same url used for + # redirection in redirect_to. + module UrlHelper + # Returns the URL for the set of +options+ provided. See the valid options in link:classes/ActionController/Base.html#M000021 + def url_for(options = {}, *parameters_for_method_reference) + if Hash === options then options = { :only_path => true }.merge(options) end + @controller.send(:url_for, options, *parameters_for_method_reference) + end + + # Creates a link tag of the given +name+ using an URL created by the set of +options+. See the valid options in + # link:classes/ActionController/Base.html#M000021. It's also possible to pass a string instead of an options hash to + # get a link tag that just points without consideration. The html_options have a special feature for creating javascript + # confirm alerts where if you pass :confirm => 'Are you sure?', the link will be guarded with a JS popup asking that question. + # If the user accepts, the link is processed, otherwise not. + def link_to(name, options = {}, html_options = {}, *parameters_for_method_reference) + convert_confirm_option_to_javascript!(html_options) unless html_options.nil? + if options.is_a?(String) + content_tag "a", name, (html_options || {}).merge({ "href" => options }) + else + content_tag("a", name, (html_options || {}).merge({ "href" => url_for(options, *parameters_for_method_reference) })) + end + end + + # Creates a link tag of the given +name+ using an URL created by the set of +options+, unless the current + # controller, action, and id are the same as the link's, in which case only the name is returned (or the + # given block is yielded, if one exists). This is useful for creating link bars where you don't want to link + # to the page currently being viewed. + def link_to_unless_current(name, options = {}, html_options = {}, *parameters_for_method_reference) + assume_current_url_options!(options) + + if destination_equal_to_current(options) + block_given? ? + yield(name, options, html_options, *parameters_for_method_reference) : + html_escape(name) + else + link_to name, options, html_options, *parameters_for_method_reference + end + end + + # Creates a link tag for starting an email to the specified <tt>email_address</tt>, which is also used as the name of the + # link unless +name+ is specified. Additional HTML options, such as class or id, can be passed in the <tt>html_options</tt> hash. + def mail_to(email_address, name = nil, html_options = {}) + content_tag "a", name || email_address, html_options.merge({ "href" => "mailto:#{email_address}" }) + end + + private + def destination_equal_to_current(options) + params_without_location = @params.reject { |key, value| %w( controller action id ).include?(key) } + + options[:action] == @params['action'] && + options[:id] == @params['id'] && + options[:controller] == @params['controller'] && + (options.has_key?(:params) ? params_without_location == options[:params] : true) + end + + def assume_current_url_options!(options) + if options[:controller].nil? + options[:controller] = @params['controller'] + if options[:action].nil? + options[:action] = @params['action'] + if options[:id].nil? then options[:id] ||= @params['id'] end + end + end + end + + def convert_confirm_option_to_javascript!(html_options) + if html_options.include?(:confirm) + html_options["onclick"] = "return confirm('#{html_options[:confirm]}');" + html_options.delete(:confirm) + end + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/partials.rb b/actionpack/lib/action_view/partials.rb new file mode 100644 index 0000000000..96bde4c6d3 --- /dev/null +++ b/actionpack/lib/action_view/partials.rb @@ -0,0 +1,64 @@ +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 the template for + # Advertiser#buy, we could have: + # + # <% for ad in @advertisements %> + # <%= render_partial "ad", ad %> + # <% end %> + # + # This would render "advertiser/_ad.rhtml" and pass the local variable +ad+ to the template for display. + # + # == Rendering a collection of partials + # + # The example of partial use describes a familar 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_collection_of_partials "ad", @advertisements %> + # + # This will render "advertiser/_ad.rhtml" and pass the local variable +ad+ to the template for display. An iteration counter + # will automatically be made available to the template with a name of the form +partial_name_counter+. In the case of the + # example above, the template would be fed +ad_counter+. + # + # == Rendering shared partials + # + # Two controllers can share a set of partials and render them like this: + # + # <%= render_partial "advertisement/ad", ad %> + # + # This will render the partial "advertisement/_ad.rhtml" regardless of which controller this is being called from. + module Partials + def render_partial(partial_path, object = nil, local_assigns = {}) + path, partial_name = partial_pieces(partial_path) + object ||= controller.instance_variable_get("@#{partial_name}") + render("#{path}/_#{partial_name}", { partial_name => object }.merge(local_assigns)) + end + + def render_collection_of_partials(partial_name, collection, partial_spacer_template = nil) + collection_of_partials = Array.new + collection.each_with_index do |element, counter| + collection_of_partials.push(render_partial(partial_name, element, "#{partial_name.split("/").last}_counter" => counter)) + end + + return nil 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 + end + end + + private + def partial_pieces(partial_path) + if partial_path.include?('/') + return File.dirname(partial_path), File.basename(partial_path) + else + return controller.send(:controller_name), partial_path + end + end + end +end diff --git a/actionpack/lib/action_view/template_error.rb b/actionpack/lib/action_view/template_error.rb new file mode 100644 index 0000000000..ab05b3303f --- /dev/null +++ b/actionpack/lib/action_view/template_error.rb @@ -0,0 +1,84 @@ +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, @file_name, @assigns, @source, @original_exception = + base_path, file_name, assigns, source, original_exception + end + + def message + if original_exception.message.include?("(eval):") + original_exception.message.scan(/\(eval\):(?:[0-9]*):in `.*'(.*)/).first.first + else + original_exception.message + end + 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 + 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 + "#{line_counter}: " + line + end + + extract.join + end + + def sub_template_of(file_name) + @sub_templates ||= [] + @sub_templates << file_name + end + + def line_number + begin + @original_exception.backtrace.join.scan(/\((?:erb)\):([0-9]*)/).first.first.to_i + rescue + begin + original_exception.message.scan(/\((?:eval)\):([0-9]*)/).first.first.to_i + rescue + 1 + end + end + end + + def file_name + strip_base_path(@file_name) + end + + def to_s + "\n\n#{self.class} (#{message}) on line ##{line_number} of #{file_name}:\n" + + source_extract + "\n " + + clean_backtrace(original_exception).join("\n ") + + "\n\n" + end + + private + def strip_base_path(file_name) + file_name.gsub(@base_path, "") + end + + def clean_backtrace(exception) + base_dir = File.expand_path(File.dirname(__FILE__) + "/../../../../") + exception.backtrace.collect { |line| line.gsub(base_dir, "").gsub("/public/../config/environments/../../", "").gsub("/public/../", "") } + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/vendor/builder.rb b/actionpack/lib/action_view/vendor/builder.rb new file mode 100644 index 0000000000..9719277669 --- /dev/null +++ b/actionpack/lib/action_view/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/actionpack/lib/action_view/vendor/builder/blankslate.rb b/actionpack/lib/action_view/vendor/builder/blankslate.rb new file mode 100644 index 0000000000..25307b0e56 --- /dev/null +++ b/actionpack/lib/action_view/vendor/builder/blankslate.rb @@ -0,0 +1,51 @@ +#!/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 <tt>\_\_send__</tt> and <tt>\_\_id__</tt>). + # BlankSlate is useful as a base class when writing classes that + # depend upon <tt>method_missing</tt> (e.g. dynamic proxies). + class BlankSlate #:nodoc: + class << self + def hide(name) + undef_method name unless 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 <em>after BlankSlate is defined</em> 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/actionpack/lib/action_view/vendor/builder/xmlbase.rb b/actionpack/lib/action_view/vendor/builder/xmlbase.rb new file mode 100644 index 0000000000..d065d6fae1 --- /dev/null +++ b/actionpack/lib/action_view/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 + # <tt><<</tt>. + # 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 <tt>method_missing</tt>. + 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 brakets as: + # + # builder.p { br; text! "HI" } #=> <p><br/>HI</p> + def text!(text) + _text(_escape(text)) + end + + # Append text to the output target without escaping any markup. + # May be used within the markup brakets as: + # + # builder.p { |x| x << "<br/>HI" } #=> <p><br/>HI</p> + # + # 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 <tt><<</tt> to append to the target, so by supporting this + # method/operation builders can use oother 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/actionpack/lib/action_view/vendor/builder/xmlevents.rb b/actionpack/lib/action_view/vendor/builder/xmlevents.rb new file mode 100644 index 0000000000..15dc7b6421 --- /dev/null +++ b/actionpack/lib/action_view/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. + # + # [<tt>start_tag(tag, attrs)</tt>] + # 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. + # + # [<tt>end_tag(tag)</tt>] + # Announces that an end tag for +tag+ has been found. + # + # [<tt>text(text)</tt>] + # 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/actionpack/lib/action_view/vendor/builder/xmlmarkup.rb b/actionpack/lib/action_view/vendor/builder/xmlmarkup.rb new file mode 100644 index 0000000000..716ff52535 --- /dev/null +++ b/actionpack/lib/action_view/vendor/builder/xmlmarkup.rb @@ -0,0 +1,288 @@ +#!/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") # => <em>emphasized</em> + # xm.em { xmm.b("emp & bold") } # => <em><b>emph & bold</b></em> + # xm.a("A Link", "href"=>"http://onestepback.org") + # # => <a href="http://onestepback.org">A Link</a> + # xm.div { br } # => <div><br/></div> + # xm.target("name"=>"compile", "option"=>"fast") + # # => <target option="fast" name="compile"\> + # # NOTE: order of attributes is not specified. + # + # xm.instruct! # <?xml version="1.0" encoding="UTF-8"?> + # xm.html { # <html> + # xm.head { # <head> + # xm.title("History") # <title>History</title> + # } # </head> + # xm.body { # <body> + # xm.comment! "HI" # <!-- HI --> + # xm.h1("Header") # <h1>Header</h1> + # xm.p("paragraph") # <p>paragraph</p> + # } # </body> + # } # </html> + # + # == Notes: + # + # * The order that attributes are inserted in markup tags is + # undefined. + # + # * Sometimes you wish to insert text without enclosing tags. Use + # the <tt>text!</tt> method to accomplish this. + # + # Example: + # + # xm.div { # <div> + # xm.text! "line"; xm.br # line<br/> + # xm.text! "another line"; xmbr # another line<br/> + # } # </div> + # + # * The special XML characters <, >, and & are converted to <, + # > and & automatically. Use the <tt><<</tt> operation to + # insert text without modification. + # + # * Sometimes tags use special characters not allowed in ruby + # identifiers. Use the <tt>tag!</tt> method to handle these + # cases. + # + # Example: + # + # xml.tag!("SOAP:Envelope") { ... } + # + # will produce ... + # + # <SOAP:Envelope> ... </SOAP:Envelope>" + # + # <tt>tag!</tt> 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. "<tt>SOAP:Envelope</tt>" => + # "<tt>xml.SOAP :Envelope</tt>") + # + # * XmlMarkup builds the markup in any object (called a _target_) + # that accepts the <tt><<</tt> 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: + # # <name> + # # <first>Jim</first> + # # <last>Weirich</last> + # # </name> + # + # * 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=><em>target_object</em>:: + # Object receiving the markup. +out+ must respond to the + # <tt><<</tt> operator. The default is a plain string target. + # :indent=><em>indentation</em>:: + # Number of spaces used for indentation. The default is no + # indentation and no line breaks. + # :margin=><em>initial_indentation_level</em>:: + # 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" + # # => <!ELEMENT blah "yada"> + def declare!(inst, *args, &block) + _indent + @target << "<!#{inst}" + args.each do |arg| + case arg + when String + @target << %{ "#{arg}"} + when Symbol + @target << " #{arg}" + end + end + if block_given? + @target << " [" + _newline + _nested_structures(block) + @target << "]" + end + @target << ">" + _newline + end + + # Insert a processing instruction into the XML markup. E.g. + # + # For example: + # + # xml.instruct! + # #=> <?xml encoding="UTF-8" version="1.0"?> + # xml.instruct! :aaa, :bbb=>"ccc" + # #=> <?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("<?#{directive_tag}", "?>", nil, attrs) + 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) + _indent + @target << open + @target << data if data + _insert_attributes(attrs) if attrs + @target << close + _newline + end + + # Start an XML tag. If <tt>end_too</tt> is true, then the start + # tag is also the end tag (e.g. <br/> + 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 << "</#{sym}>" + end + + # Insert the attributes (given in the hash). + def _insert_attributes(attrs) + return if attrs.nil? + attrs.each do |k, v| + @target << %{ #{k}="#{v}"} + 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/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb new file mode 100644 index 0000000000..0fcb5e852d --- /dev/null +++ b/actionpack/test/abstract_unit.rb @@ -0,0 +1,9 @@ +$:.unshift(File.dirname(__FILE__) + '/../lib') + +require 'test/unit' +require 'action_controller' + +require 'action_controller/test_process' + +ActionController::Base.logger = nil +ActionController::Base.ignore_missing_templates = true
\ No newline at end of file diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb new file mode 100644 index 0000000000..6d727be5a2 --- /dev/null +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -0,0 +1,323 @@ +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 + + # 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_text "Mr. Henke" + end + + def render_based_on_parameters + render_text "Mr. #{@params["name"]}" + end + + # puts something in the session + def session_stuffing + session['xmas'] = 'turkey' + render_text "ho ho ho" + end + + # 911 + def rescue_action(e) raise; 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 ------------------------------------------------ + + # 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 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 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 ActionController::TestResponse.assertion_target.has_flash_with_contents? + 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_not_exists + 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' + assert_rendered_file + end + + # test the assert_success assertion + def test_assert_success + process :nothing + assert_success + end + + # -- standard request/reponse 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 '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_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 +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('text/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/actionpack/test/controller/active_record_assertions_test.rb b/actionpack/test/controller/active_record_assertions_test.rb new file mode 100644 index 0000000000..53106aaee7 --- /dev/null +++ b/actionpack/test/controller/active_record_assertions_test.rb @@ -0,0 +1,119 @@ +path_to_ar = File.dirname(__FILE__) + '/../../../activerecord' + +if Object.const_defined?("ActiveRecord") || File.exist?(path_to_ar) +# This test is very different than the others. It requires ActiveRecord to +# run. There's a bunch of stuff we are assuming here: +# +# 1. activerecord exists as a sibling directory to actionpack +# (i.e., actionpack/../activerecord) +# 2. you've created the appropriate database to run the active_record unit tests +# 3. you set the appropriate database connection below + +driver_to_use = 'native_sqlite' + +$: << path_to_ar + '/lib/' +$: << path_to_ar + '/test/' +require 'active_record' unless Object.const_defined?("ActiveRecord") +require "connections/#{driver_to_use}/connection" +require 'fixtures/company' + +# ----------------------------------------------------------------------------- + +# add some validation rules to trip up the assertions +class Company + def validate + errors.add_on_empty('name') + errors.add('rating', 'rating should not be 2') if rating == 2 + errors.add_to_base('oh oh') if rating == 3 + end +end + +# ----------------------------------------------------------------------------- + +require File.dirname(__FILE__) + '/../abstract_unit' + +# a controller class to handle the AR assertions +class ActiveRecordAssertionsController < ActionController::Base + # fail with 1 bad column + def nasty_columns_1 + @company = Company.new + @company.name = "B" + @company.rating = 2 + render_text "snicker...." + end + + # fail with 2 bad column + def nasty_columns_2 + @company = Company.new + @company.name = "" + @company.rating = 2 + render_text "double snicker...." + end + + # this will pass validation + def good_company + @company = Company.new + @company.name = "A" + @company.rating = 69 + render_text "Goodness Gracious!" + end + + # this will fail validation + def bad_company + @company = Company.new + render_text "Who's Bad?" + end + + # the safety dance...... + def rescue_action(e) raise; end +end + +# ----------------------------------------------------------------------------- + +ActiveRecordAssertionsController.template_root = File.dirname(__FILE__) + "/../fixtures/" + +# The test case to try the AR assertions +class ActiveRecordAssertionsControllerTest < Test::Unit::TestCase + # set it up + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @controller = ActiveRecordAssertionsController.new + 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', ['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', ['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 + +end
\ No newline at end of file diff --git a/actionpack/test/controller/cgi_test.rb b/actionpack/test/controller/cgi_test.rb new file mode 100755 index 0000000000..46e24ab403 --- /dev/null +++ b/actionpack/test/controller/cgi_test.rb @@ -0,0 +1,142 @@ +$:.unshift(File.dirname(__FILE__) + '/../../lib') + +require 'test/unit' +require 'action_controller/cgi_ext/cgi_methods' +require 'stringio' + +class MockUploadedFile < StringIO + def content_type + "img/jpeg" + end + + def original_filename + "my_file.doc" + end +end + +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" + 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_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_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 + mock_file = MockUploadedFile.new + + input = { + "something" => [ StringIO.new("") ], + "products[string]" => [ StringIO.new("Apple Computer") ], + "products[file]" => [ mock_file ] + } + + expected_output = { + "something" => "", + "products" => { + "string" => "Apple Computer", + "file" => mock_file + } + } + + assert_equal expected_output, CGIMethods.parse_request_parameters(input) + 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 +end diff --git a/actionpack/test/controller/cookie_test.rb b/actionpack/test/controller/cookie_test.rb new file mode 100644 index 0000000000..d3099bcd99 --- /dev/null +++ b/actionpack/test/controller/cookie_test.rb @@ -0,0 +1,38 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class CookieTest < Test::Unit::TestCase + class TestController < ActionController::Base + def authenticate + cookie "name" => "user_name", "value" => "david" + render_text "hello world" + end + + def access_frozen_cookies + @cookies["wont"] = "work" + 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 + @request.action = "authenticate" + assert_equal [ CGI::Cookie::new("name" => "user_name", "value" => "david") ], process_request.headers["cookie"] + end + + def test_setting_cookie + @request.action = "access_frozen_cookies" + assert_raises(TypeError) { process_request } + end + + private + def process_request + TestController.process(@request, @response) + end +end
\ No newline at end of file diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb new file mode 100644 index 0000000000..f4d7a689b5 --- /dev/null +++ b/actionpack/test/controller/filters_test.rb @@ -0,0 +1,159 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class FilterTest < Test::Unit::TestCase + class TestController < ActionController::Base + before_filter :ensure_login + + def show + render_text "ran action" + end + + private + def ensure_login + @ran_filter ||= [] + @ran_filter << "ensure_login" + end + end + + class PrependingController < TestController + prepend_before_filter :wonderful_life + + private + def wonderful_life + @ran_filter ||= [] + @ran_filter << "wonderful_life" + end + 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 + + + def test_added_filter_to_inheritance_graph + assert_equal [ :fire_flash, :ensure_login ], TestController.before_filters + end + + def test_base_class_in_isolation + assert_equal [ :fire_flash ], ActionController::Base.before_filters + end + + def test_prepending_filter + assert_equal [ :wonderful_life, :fire_flash, :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_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 + + private + def test_process(controller) + request = ActionController::TestRequest.new + request.action = "show" + controller.process(request, ActionController::TestResponse.new) + end +end
\ No newline at end of file diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb new file mode 100644 index 0000000000..033477fe39 --- /dev/null +++ b/actionpack/test/controller/flash_test.rb @@ -0,0 +1,69 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class FlashTest < Test::Unit::TestCase + class TestController < ActionController::Base + def set_flash + flash["that"] = "hello" + render_text "hello" + end + + def use_flash + @flashy = flash["that"] + render_text "hello" + end + + def use_flash_and_keep_it + @flashy = flash["that"] + keep_flash + render_text "hello" + end + + def rescue_action(e) + raise unless ActionController::MissingTemplate === e + end + end + + def setup + initialize_request_and_response + end + + def test_flash + @request.action = "set_flash" + response = process_request + + @request.action = "use_flash" + first_response = process_request + assert_equal "hello", first_response.template.assigns["flash"]["that"] + assert_equal "hello", first_response.template.assigns["flashy"] + + second_response = process_request + assert_nil second_response.template.assigns["flash"]["that"], "On second flash" + end + + def test_keep_flash + @request.action = "set_flash" + response = process_request + + @request.action = "use_flash_and_keep_it" + first_response = process_request + assert_equal "hello", first_response.template.assigns["flash"]["that"] + assert_equal "hello", first_response.template.assigns["flashy"] + + @request.action = "use_flash" + second_response = process_request + assert_equal "hello", second_response.template.assigns["flash"]["that"], "On second flash" + + third_response = process_request + assert_nil third_response.template.assigns["flash"]["that"], "On third flash" + end + + private + def initialize_request_and_response + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def process_request + TestController.process(@request, @response) + end +end
\ No newline at end of file diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb new file mode 100644 index 0000000000..9d1da53241 --- /dev/null +++ b/actionpack/test/controller/helper_test.rb @@ -0,0 +1,110 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class HelperTest < Test::Unit::TestCase + HELPER_PATHS = %w(/../fixtures/helpers) + + class TestController < ActionController::Base + attr_accessor :delegate_attr + def delegate_method() end + def rescue_action(e) raise end + end + + module LocalAbcHelper + def a() end + def b() end + def c() end + end + + + 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 + + # Add helper paths to LOAD_PATH. + HELPER_PATHS.each { |path| + $LOAD_PATH.unshift(File.dirname(__FILE__) + path) + } + + # Set default test helper. + self.test_helper = LocalAbcHelper + end + + def teardown + # Reset template class. + #ActionController::Base.template_class = ActionView::Base + + # Remove helper paths from LOAD_PATH. + HELPER_PATHS.each { |path| + $LOAD_PATH.delete(File.dirname(__FILE__) + path) + } + end + + + def test_deprecated_helper + assert_equal 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 helper_methods, missing_methods + assert_nothing_raised { @controller_class.helper :abc } + assert_equal [], missing_methods + end + + def test_declare_missing_helper + assert_equal helper_methods, missing_methods + assert_raise(LoadError) { @controller_class.helper :missing } + end + + def test_helper_block + assert_nothing_raised { + @controller_class.helper { def block_helper_method; end } + } + assert template_methods.include?('block_helper_method') + end + + def test_helper_block_include + assert_equal 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 template_methods.include?('delegate_method') + end + + def test_helper_attr + assert_nothing_raised { @controller_class.helper_attr :delegate_attr } + assert template_methods.include?('delegate_attr') + assert template_methods.include?('delegate_attr=') + end + + + private + def helper_methods; TestHelper.instance_methods end + def template_methods; @template_class.instance_methods end + def missing_methods; helper_methods - template_methods end + + def test_helper=(helper_module) + old_verbose, $VERBOSE = $VERBOSE, nil + self.class.const_set('TestHelper', helper_module) + $VERBOSE = old_verbose + end +end
\ No newline at end of file diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb new file mode 100644 index 0000000000..f652453ebd --- /dev/null +++ b/actionpack/test/controller/layout_test.rb @@ -0,0 +1,49 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class TestLayoutController < ActionController::Base + layout "layouts/standard" + + def hello_world + end + + def hello_world_outside_layout + end + + def rescue_action(e) raise end +end + +class ChildWithoutTestLayoutController < TestLayoutController + layout nil + + def hello_world + end +end + +class ChildWithOtherTestLayoutController < TestLayoutController + layout nil + + def hello_world + end +end + +class RenderTest < Test::Unit::TestCase + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + + @request.host = "www.nextangle.com" + end + + def test_layout_rendering + @request.action = "hello_world" + response = process_request + assert_equal "200 OK", response.headers["Status"] + assert_equal "layouts/standard", response.template.template_name + end + + + private + def process_request + TestLayoutController.process(@request, @response) + end +end
\ No newline at end of file diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb new file mode 100755 index 0000000000..6302016a53 --- /dev/null +++ b/actionpack/test/controller/redirect_test.rb @@ -0,0 +1,44 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class RedirectTest < Test::Unit::TestCase + class RedirectController < ActionController::Base + def simple_redirect + redirect_to :action => "hello_world" + end + + def method_redirect + redirect_to :dashbord_url, 1, "hello" + end + + def rescue_errors(e) raise e end + + protected + def dashbord_url(id, message) + url_for :action => "dashboard", :params => { "id" => id, "message" => message } + end + end + + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_simple_redirect + @request.path = "/redirect/simple_redirect" + @request.action = "simple_redirect" + response = process_request + assert_equal "http://test.host/redirect/hello_world", response.headers["location"] + end + + def test_redirect_with_method_reference_and_parameters + @request.path = "/redirect/method_redirect" + @request.action = "method_redirect" + response = process_request + assert_equal "http://test.host/redirect/dashboard?message=hello&id=1", response.headers["location"] + end + + private + def process_request + RedirectController.process(@request, @response) + end +end
\ No newline at end of file diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb new file mode 100644 index 0000000000..ce778e1d7d --- /dev/null +++ b/actionpack/test/controller/render_test.rb @@ -0,0 +1,178 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +Customer = Struct.new("Customer", :name) + +class RenderTest < Test::Unit::TestCase + 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_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 + @customers = [ Customer.new("david"), Customer.new("mary") ] + render_action "list" + end + + def modgreet + 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/" + + class TestLayoutController < ActionController::Base + layout "layouts/standard" + + def hello_world + end + + def hello_world_outside_layout + end + + def rescue_action(e) + raise unless ActionController::MissingTemplate === e + end + end + + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + + @request.host = "www.nextangle.com" + end + + def test_simple_show + @request.action = "hello_world" + response = process_request + assert_equal "200 OK", response.headers["Status"] + assert_equal "test/hello_world", response.template.first_render + end + + def test_do_with_render + @request.action = "render_hello_world" + assert_equal "test/hello_world", process_request.template.first_render + end + + def test_do_with_render_from_variable + @request.action = "render_hello_world_from_variable" + assert_equal "hello david", process_request.body + end + + def test_do_with_render_action + @request.action = "render_action_hello_world" + assert_equal "test/hello_world", process_request.template.first_render + end + + def test_do_with_render_text + @request.action = "render_text_hello_world" + assert_equal "hello world", process_request.body + end + + def test_do_with_render_custom_code + @request.action = "render_custom_code" + assert_equal "404 Moved", process_request.headers["Status"] + end + + def test_attempt_to_access_object_method + @request.action = "clone" + assert_raises(ActionController::UnknownAction, "No action responded to [clone]") { process_request } + end + + def test_access_to_request_in_view + ActionController::Base.view_controller_internals = false + + @request.action = "hello_world" + response = process_request + assert_nil response.template.assigns["request"] + + ActionController::Base.view_controller_internals = true + + @request.action = "hello_world" + response = process_request + assert_kind_of ActionController::AbstractRequest, response.template.assigns["request"] + end + + def test_render_xml + @request.action = "render_xml_hello" + assert_equal "<html>\n <p>Hello David</p>\n<p>This is grand!</p>\n</html>\n", process_request.body + end + + def test_render_xml_with_default + @request.action = "greeting" + assert_equal "<p>This is grand!</p>\n", process_request.body + end + + def test_layout_rendering + @request.action = "layout_test" + assert_equal "<html>Hello world!</html>", process_request.body + end + + def test_render_xml_with_layouts + @request.action = "builder_layout_test" + assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", process_request.body + end + + def test_partials_list + @request.action = "partials_list" + assert_equal "Hello: davidHello: mary", process_request.body + end + + def test_module_rendering + @request.action = "modgreet" + @request.parameters["module"] = "scope" + assert_equal "<p>Beautiful modules!</p>", process_request.body + end + + private + def process_request + TestController.process(@request, @response) + end +end
\ No newline at end of file diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb new file mode 100644 index 0000000000..57205a01c5 --- /dev/null +++ b/actionpack/test/controller/send_file_test.rb @@ -0,0 +1,68 @@ +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 + + 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 + + +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 + + old_stdout = $stdout + begin + require 'stringio' + $stdout = StringIO.new + $stdout.binmode + assert_nothing_raised { response.body.call } + assert_equal file_data, $stdout.string + ensure + $stdout = old_stdout + end + 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 +end diff --git a/actionpack/test/controller/url_test.rb b/actionpack/test/controller/url_test.rb new file mode 100644 index 0000000000..bf6a7aab75 --- /dev/null +++ b/actionpack/test/controller/url_test.rb @@ -0,0 +1,368 @@ +require File.dirname(__FILE__) + '/../abstract_unit' +require 'action_controller/url_rewriter' + +MockRequest = Struct.new("MockRequest", :protocol, :host, :port, :path, :parameters) +class MockRequest + def host_with_port + if (protocol == "http://" && port == 80) || (protocol == "https://" && port == 443) + host + else + host + ":#{port}" + end + end +end + +class UrlTest < Test::Unit::TestCase + def setup + @library_url = ActionController::UrlRewriter.new(MockRequest.new( + "http://", + "www.singlefile.com", + 80, + "/library/books/ISBN/0743536703/show", + { "type" => "ISBN", "code" => "0743536703" } + ), "books", "show") + + @library_url_on_index = ActionController::UrlRewriter.new(MockRequest.new( + "http://", + "www.singlefile.com", + 80, + "/library/books/ISBN/0743536703/", + { "type" => "ISBN", "code" => "0743536703" } + ), "books", "index") + + @clean_urls = [ + ActionController::UrlRewriter.new(MockRequest.new( + "http://", "www.singlefile.com", 80, "/identity/", {} + ), "identity", "index"), + ActionController::UrlRewriter.new(MockRequest.new( + "http://", "www.singlefile.com", 80, "/identity", {} + ), "identity", "index") + ] + + @clean_url_with_id = ActionController::UrlRewriter.new(MockRequest.new( + "http://", "www.singlefile.com", 80, "/identity/show/5", { "id" => "5" } + ), "identity", "show") + + @clean_url_with_id_as_char = ActionController::UrlRewriter.new(MockRequest.new( + "http://", "www.singlefile.com", 80, "/teachers/show/t", { "id" => "t" } + ), "teachers", "show") + end + + def test_clean_action + assert_equal "http://www.singlefile.com/library/books/ISBN/0743536703/edit", @library_url.rewrite(:action => "edit") + end + + def test_clean_action_with_only_path + assert_equal "/library/books/ISBN/0743536703/edit", @library_url.rewrite(:action => "edit", :only_path => true) + end + + def test_action_from_index + assert_equal "http://www.singlefile.com/library/books/ISBN/0743536703/edit", @library_url_on_index.rewrite(:action => "edit") + end + + def test_action_from_index_on_clean + @clean_urls.each do |url| + assert_equal "http://www.singlefile.com/identity/edit", url.rewrite(:action => "edit") + end + end + + def test_action_without_prefix + assert_equal "http://www.singlefile.com/library/books/", @library_url.rewrite(:action => "index", :action_prefix => "") + end + + def test_action_with_prefix + assert_equal( + "http://www.singlefile.com/library/books/XTC/123/show", + @library_url.rewrite(:action => "show", :action_prefix => "XTC/123") + ) + end + + def test_action_prefix_alone + assert_equal( + "http://www.singlefile.com/library/books/XTC/123/", + @library_url.rewrite(:action_prefix => "XTC/123") + ) + end + + def test_action_with_suffix + assert_equal( + "http://www.singlefile.com/library/books/show/XTC/123", + @library_url.rewrite(:action => "show", :action_prefix => "", :action_suffix => "XTC/123") + ) + end + + def test_clean_controller + assert_equal "http://www.singlefile.com/library/settings/", @library_url.rewrite(:controller => "settings") + end + + def test_clean_controller_prefix + assert_equal "http://www.singlefile.com/shop/", @library_url.rewrite(:controller_prefix => "shop") + end + + def test_clean_controller_with_module + assert_equal "http://www.singlefile.com/shop/purchases/", @library_url.rewrite(:module => "shop", :controller => "purchases") + end + + def test_controller_and_action + assert_equal "http://www.singlefile.com/library/settings/show", @library_url.rewrite(:controller => "settings", :action => "show") + end + + def test_controller_and_action_and_anchor + assert_equal( + "http://www.singlefile.com/library/settings/show#5", + @library_url.rewrite(:controller => "settings", :action => "show", :anchor => "5") + ) + end + + def test_controller_and_action_and_empty_overwrite_params_and_anchor + assert_equal( + "http://www.singlefile.com/library/settings/show?code=0743536703&type=ISBN#5", + @library_url.rewrite(:controller => "settings", :action => "show", :overwrite_params => {}, :anchor => "5") + ) + end + + def test_controller_and_action_and_overwrite_params_and_anchor + assert_equal( + "http://www.singlefile.com/library/settings/show?code=0000001&type=ISBN#5", + @library_url.rewrite(:controller => "settings", :action => "show", :overwrite_params => {"code"=>"0000001"}, :anchor => "5") + ) + end + + def test_controller_and_action_and_overwrite_params_with_nil_value_and_anchor + assert_equal( + "http://www.singlefile.com/library/settings/show?type=ISBN#5", + @library_url.rewrite(:controller => "settings", :action => "show", :overwrite_params => {"code" => nil}, :anchor => "5") + ) + end + + def test_controller_and_action_params_and_overwrite_params_and_anchor + assert_equal( + "http://www.singlefile.com/library/settings/show?code=0000001&version=5.0#5", + @library_url.rewrite(:controller => "settings", :action => "show", :params=>{"version" => "5.0"}, :overwrite_params => {"code"=>"0000001"}, :anchor => "5") + ) + end + + def test_controller_and_action_and_params_anchor + assert_equal( + "http://www.singlefile.com/library/settings/show?update=1#5", + @library_url.rewrite(:controller => "settings", :action => "show", :params => { "update" => "1"}, :anchor => "5") + ) + end + + def test_controller_and_index_action + assert_equal "http://www.singlefile.com/library/settings/", @library_url.rewrite(:controller => "settings", :action => "index") + end + + def test_controller_and_action_with_same_name_as_controller + @clean_urls.each do |url| + assert_equal "http://www.singlefile.com/anything/identity", url.rewrite(:controller => "anything", :action => "identity") + end + end + + def test_controller_and_index_action_without_controller_prefix + assert_equal( + "http://www.singlefile.com/settings/", + @library_url.rewrite(:controller => "settings", :action => "index", :controller_prefix => "") + ) + end + + def test_controller_and_index_action_with_controller_prefix + assert_equal( + "http://www.singlefile.com/fantastic/settings/show", + @library_url.rewrite(:controller => "settings", :action => "show", :controller_prefix => "fantastic") + ) + end + + def test_path_parameters + assert_equal "http://www.singlefile.com/library/books/EXBC/0743536703/show", @library_url.rewrite(:path_params => {"type" => "EXBC"}) + end + + def test_parameters + assert_equal( + "http://www.singlefile.com/library/books/ISBN/0743536703/show?delete=1&name=David", + @library_url.rewrite(:params => {"delete" => "1", "name" => "David"}) + ) + end + + def test_parameters_with_id + @clean_urls.each do |url| + assert_equal( + "http://www.singlefile.com/identity/show?name=David&id=5", + url.rewrite( + :action => "show", + :params => { "id" => "5", "name" => "David" } + ) + ) + end + end + + def test_action_with_id + assert_equal( + "http://www.singlefile.com/identity/show/7", + @clean_url_with_id.rewrite( + :action => "show", + :id => 7 + ) + ) + @clean_urls.each do |url| + assert_equal( + "http://www.singlefile.com/identity/index/7", + url.rewrite(:id => 7) + ) + end + end + + def test_parameters_with_id_and_away + assert_equal( + "http://www.singlefile.com/identity/show/25?name=David", + @clean_url_with_id.rewrite( + :path_params => { "id" => "25" }, + :params => { "name" => "David" } + ) + ) + end + + def test_parameters_with_index_and_id + @clean_urls.each do |url| + assert_equal( + "http://www.singlefile.com/identity/index/25?name=David", + url.rewrite( + :path_params => { "id" => "25" }, + :params => { "name" => "David" } + ) + ) + end + end + + def test_action_going_away_from_id + assert_equal( + "http://www.singlefile.com/identity/list", + @clean_url_with_id.rewrite( + :action => "list" + ) + ) + end + + def test_parameters_with_direct_id_and_away + assert_equal( + "http://www.singlefile.com/identity/show/25?name=David", + @clean_url_with_id.rewrite( + :id => "25", + :params => { "name" => "David" } + ) + ) + end + + def test_parameters_with_direct_id_and_away + assert_equal( + "http://www.singlefile.com/store/open/25?name=David", + @clean_url_with_id.rewrite( + :controller => "store", + :action => "open", + :id => "25", + :params => { "name" => "David" } + ) + ) + end + + def test_parameters_to_id + @clean_urls.each do |url| + %w(show index).each do |action| + assert_equal( + "http://www.singlefile.com/identity/#{action}/25?name=David", + url.rewrite( + :action => action, + :path_params => { "id" => "25" }, + :params => { "name" => "David" } + ) + ) + end + end + end + + def test_parameters_from_id + assert_equal( + "http://www.singlefile.com/identity/", + @clean_url_with_id.rewrite( + :action => "index" + ) + ) + end + + def test_id_as_char_and_part_of_controller + assert_equal( + "http://www.singlefile.com/teachers/skill/5", + @clean_url_with_id_as_char.rewrite( + :action => "skill", + :id => 5 + ) + ) + end + + def test_from_clean_to_library + @clean_urls.each do |url| + assert_equal( + "http://www.singlefile.com/library/books/ISBN/0743536703/show?delete=1&name=David", + url.rewrite( + :controller_prefix => "library", + :controller => "books", + :action_prefix => "ISBN/0743536703", + :action => "show", + :params => { "delete" => "1", "name" => "David" } + ) + ) + end + end + + def test_from_library_to_clean + assert_equal( + "http://www.singlefile.com/identity/", + @library_url.rewrite( + :controller => "identity", :controller_prefix => "" + ) + ) + end + + def test_from_another_port + @library_url = ActionController::UrlRewriter.new(MockRequest.new( + "http://", + "www.singlefile.com", + 8080, + "/library/books/ISBN/0743536703/show", + { "type" => "ISBN", "code" => "0743536703" } + ), "books", "show") + + assert_equal( + "http://www.singlefile.com:8080/identity/", + @library_url.rewrite( + :controller => "identity", :controller_prefix => "" + ) + ) + end + + def test_basecamp + basecamp_url = ActionController::UrlRewriter.new(MockRequest.new( + "http://", + "projects.basecamp", + 80, + "/clients/disarray/1/msg/transcripts/", + {"category_name"=>"transcripts", "client_name"=>"disarray", "action"=>"index", "controller"=>"msg", "project_name"=>"1"} + ), "msg", "index") + + assert_equal( + "http://projects.basecamp/clients/disarray/1/msg/transcripts/1/comments", + basecamp_url.rewrite(:action_prefix => "transcripts/1", :action => "comments") + ) + end + + def test_on_explicit_index_page # My index page is very modest, thank you... + url = ActionController::UrlRewriter.new( + MockRequest.new( + "http://", "example.com", 80, "/controller/index", + {"controller"=>"controller", "action"=>"index"} + ), "controller", "index" + ) + assert_equal("http://example.com/controller/foo", url.rewrite(:action => 'foo')) + end + +end diff --git a/actionpack/test/fixtures/helpers/abc_helper.rb b/actionpack/test/fixtures/helpers/abc_helper.rb new file mode 100644 index 0000000000..7104ff3730 --- /dev/null +++ b/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/actionpack/test/fixtures/layouts/builder.rxml b/actionpack/test/fixtures/layouts/builder.rxml new file mode 100644 index 0000000000..729af4b8bc --- /dev/null +++ b/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/actionpack/test/fixtures/layouts/standard.rhtml b/actionpack/test/fixtures/layouts/standard.rhtml new file mode 100644 index 0000000000..fcb28ec755 --- /dev/null +++ b/actionpack/test/fixtures/layouts/standard.rhtml @@ -0,0 +1 @@ +<html><%= @content_for_layout %></html>
\ No newline at end of file diff --git a/actionpack/test/fixtures/scope/test/modgreet.rhtml b/actionpack/test/fixtures/scope/test/modgreet.rhtml new file mode 100644 index 0000000000..8947726e89 --- /dev/null +++ b/actionpack/test/fixtures/scope/test/modgreet.rhtml @@ -0,0 +1 @@ +<p>Beautiful modules!</p>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_customer.rhtml b/actionpack/test/fixtures/test/_customer.rhtml new file mode 100644 index 0000000000..872d8c44e6 --- /dev/null +++ b/actionpack/test/fixtures/test/_customer.rhtml @@ -0,0 +1 @@ +Hello: <%= customer.name %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/greeting.rhtml b/actionpack/test/fixtures/test/greeting.rhtml new file mode 100644 index 0000000000..62fb0293f0 --- /dev/null +++ b/actionpack/test/fixtures/test/greeting.rhtml @@ -0,0 +1 @@ +<p>This is grand!</p> diff --git a/actionpack/test/fixtures/test/hello.rxml b/actionpack/test/fixtures/test/hello.rxml new file mode 100644 index 0000000000..82a4a310d3 --- /dev/null +++ b/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/actionpack/test/fixtures/test/hello_world.rhtml b/actionpack/test/fixtures/test/hello_world.rhtml new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/actionpack/test/fixtures/test/hello_world.rhtml @@ -0,0 +1 @@ +Hello world!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/hello_xml_world.rxml b/actionpack/test/fixtures/test/hello_xml_world.rxml new file mode 100644 index 0000000000..02b14fe87c --- /dev/null +++ b/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/actionpack/test/fixtures/test/list.rhtml b/actionpack/test/fixtures/test/list.rhtml new file mode 100644 index 0000000000..39e2cad966 --- /dev/null +++ b/actionpack/test/fixtures/test/list.rhtml @@ -0,0 +1 @@ +<%= render_collection_of_partials "customer", @customers %>
\ No newline at end of file diff --git a/actionpack/test/template/active_record_helper_test.rb b/actionpack/test/template/active_record_helper_test.rb new file mode 100644 index 0000000000..4b32f4dd48 --- /dev/null +++ b/actionpack/test/template/active_record_helper_test.rb @@ -0,0 +1,76 @@ +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/active_record_helper' + +class ActiveRecordHelperTest < Test::Unit::TestCase + include ActionView::Helpers::FormHelper + include ActionView::Helpers::ActiveRecordHelper + + Post = Struct.new("Post", :title, :author_name, :body, :secret, :written_on) + Column = Struct.new("Column", :type, :name, :human_name) + + def setup + @post = Post.new + def @post.errors() Class.new{ def on(field) field == "author_name" || field == "body" end }.new end + def @post.new_record?() true 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) + end + + def test_generic_input_tag + assert_equal( + '<input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />', input("post", "title") + ) + end + + def test_text_area_with_errors + assert_equal( + "<div class=\"fieldWithErrors\"><textarea cols=\"40\" id=\"post_body\" name=\"post[body]\" rows=\"20\" wrap=\"virtual\">Back to the hill and over it again!</textarea></div>", + text_area("post", "body") + ) + end + + def test_text_field_with_errors + assert_equal( + '<div class="fieldWithErrors"><input id="post_author_name" name="post[author_name]" size="30" type="text" value="" /></div>', + text_field("post", "author_name") + ) + end + + def test_form_with_string + assert_equal( + "<form action='create' method='POST'><p><label for=\"post_title\">Title</label><br /><input id=\"post_title\" name=\"post[title]\" size=\"30\" type=\"text\" value=\"Hello World\" /></p>\n<p><label for=\"post_body\">Body</label><br /><div class=\"fieldWithErrors\"><textarea cols=\"40\" id=\"post_body\" name=\"post[body]\" rows=\"20\" wrap=\"virtual\">Back to the hill and over it again!</textarea></div></p><input type='submit' value='Create' /></form>", + form("post") + ) + end + + def test_form_with_date + def Post.content_columns() [ Column.new(:date, "written_on", "Written on") ] end + + assert_equal( + "<form action='create' method='POST'><p><label for=\"post_written_on\">Written on</label><br /><select name='post[written_on(1i)]'>\n<option>1999</option>\n<option>2000</option>\n<option>2001</option>\n<option>2002</option>\n<option>2003</option>\n<option selected=\"selected\">2004</option>\n<option>2005</option>\n<option>2006</option>\n<option>2007</option>\n<option>2008</option>\n<option>2009</option>\n</select>\n<select name='post[written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option value='6' selected=\"selected\">June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n<select name='post[written_on(3i)]'>\n<option>1</option>\n<option>2</option>\n<option>3</option>\n<option>4</option>\n<option>5</option>\n<option>6</option>\n<option>7</option>\n<option>8</option>\n<option>9</option>\n<option>10</option>\n<option>11</option>\n<option>12</option>\n<option>13</option>\n<option>14</option>\n<option selected=\"selected\">15</option>\n<option>16</option>\n<option>17</option>\n<option>18</option>\n<option>19</option>\n<option>20</option>\n<option>21</option>\n<option>22</option>\n<option>23</option>\n<option>24</option>\n<option>25</option>\n<option>26</option>\n<option>27</option>\n<option>28</option>\n<option>29</option>\n<option>30</option>\n<option>31</option>\n</select>\n</p><input type='submit' value='Create' /></form>", + 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_equal( + "<form action='create' method='POST'><p><label for=\"post_written_on\">Written on</label><br /><select name='post[written_on(1i)]'>\n<option>1999</option>\n<option>2000</option>\n<option>2001</option>\n<option>2002</option>\n<option>2003</option>\n<option selected=\"selected\">2004</option>\n<option>2005</option>\n<option>2006</option>\n<option>2007</option>\n<option>2008</option>\n<option>2009</option>\n</select>\n<select name='post[written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option value='6' selected=\"selected\">June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n<select name='post[written_on(3i)]'>\n<option>1</option>\n<option>2</option>\n<option>3</option>\n<option>4</option>\n<option>5</option>\n<option>6</option>\n<option>7</option>\n<option>8</option>\n<option>9</option>\n<option>10</option>\n<option>11</option>\n<option>12</option>\n<option>13</option>\n<option>14</option>\n<option selected=\"selected\">15</option>\n<option>16</option>\n<option>17</option>\n<option>18</option>\n<option>19</option>\n<option>20</option>\n<option>21</option>\n<option>22</option>\n<option>23</option>\n<option>24</option>\n<option>25</option>\n<option>26</option>\n<option>27</option>\n<option>28</option>\n<option>29</option>\n<option>30</option>\n<option>31</option>\n</select>\n — <select name='post[written_on(4i)]'>\n<option>00</option>\n<option>01</option>\n<option>02</option>\n<option>03</option>\n<option>04</option>\n<option>05</option>\n<option>06</option>\n<option>07</option>\n<option>08</option>\n<option>09</option>\n<option>10</option>\n<option>11</option>\n<option>12</option>\n<option>13</option>\n<option>14</option>\n<option>15</option>\n<option selected=\"selected\">16</option>\n<option>17</option>\n<option>18</option>\n<option>19</option>\n<option>20</option>\n<option>21</option>\n<option>22</option>\n<option>23</option>\n</select>\n : <select name='post[written_on(5i)]'>\n<option>00</option>\n<option>01</option>\n<option>02</option>\n<option>03</option>\n<option>04</option>\n<option>05</option>\n<option>06</option>\n<option>07</option>\n<option>08</option>\n<option>09</option>\n<option>10</option>\n<option>11</option>\n<option>12</option>\n<option>13</option>\n<option>14</option>\n<option>15</option>\n<option>16</option>\n<option>17</option>\n<option>18</option>\n<option>19</option>\n<option>20</option>\n<option>21</option>\n<option>22</option>\n<option>23</option>\n<option>24</option>\n<option>25</option>\n<option>26</option>\n<option>27</option>\n<option>28</option>\n<option>29</option>\n<option selected=\"selected\">30</option>\n<option>31</option>\n<option>32</option>\n<option>33</option>\n<option>34</option>\n<option>35</option>\n<option>36</option>\n<option>37</option>\n<option>38</option>\n<option>39</option>\n<option>40</option>\n<option>41</option>\n<option>42</option>\n<option>43</option>\n<option>44</option>\n<option>45</option>\n<option>46</option>\n<option>47</option>\n<option>48</option>\n<option>49</option>\n<option>50</option>\n<option>51</option>\n<option>52</option>\n<option>53</option>\n<option>54</option>\n<option>55</option>\n<option>56</option>\n<option>57</option>\n<option>58</option>\n<option>59</option>\n</select>\n</p><input type='submit' value='Create' /></form>", + form("post") + ) + end +end
\ No newline at end of file diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb new file mode 100755 index 0000000000..a8ad37918d --- /dev/null +++ b/actionpack/test/template/date_helper_test.rb @@ -0,0 +1,104 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/date_helper' + +class DateHelperTest < Test::Unit::TestCase + include ActionView::Helpers::DateHelper + + 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)) + end + + def test_select_day + expected = "<select name='date[day]'>\n" + expected << +"<option>1</option>\n<option>2</option>\n<option>3</option>\n<option>4</option>\n<option>5</option>\n<option>6</option>\n<option>7</option>\n<option>8</option>\n<option>9</option>\n<option>10</option>\n<option>11</option>\n<option>12</option>\n<option>13</option>\n<option>14</option>\n<option>15</option>\n<option selected=\"selected\">16</option>\n<option>17</option>\n<option>18</option>\n<option>19</option>\n<option>20</option>\n<option>21</option>\n<option>22</option>\n<option>23</option>\n<option>24</option>\n<option>25</option>\n<option>26</option>\n<option>27</option>\n<option>28</option>\n<option>29</option>\n<option>30</option>\n<option>31</option>\n" + expected << "</select>\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 = "<select name='date[day]'>\n" + expected << +"<option></option>\n<option>1</option>\n<option>2</option>\n<option>3</option>\n<option>4</option>\n<option>5</option>\n<option>6</option>\n<option>7</option>\n<option>8</option>\n<option>9</option>\n<option>10</option>\n<option>11</option>\n<option>12</option>\n<option>13</option>\n<option>14</option>\n<option>15</option>\n<option selected=\"selected\">16</option>\n<option>17</option>\n<option>18</option>\n<option>19</option>\n<option>20</option>\n<option>21</option>\n<option>22</option>\n<option>23</option>\n<option>24</option>\n<option>25</option>\n<option>26</option>\n<option>27</option>\n<option>28</option>\n<option>29</option>\n<option>30</option>\n<option>31</option>\n" + expected << "</select>\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_month + expected = "<select name='date[month]'>\n" + expected << "<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option value='6'>June</option>\n<option value='7'>July</option>\n<option value='8' selected=\"selected\">August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n" + expected << "</select>\n" + + assert_equal expected, select_month(Time.mktime(2003, 8, 16)) + assert_equal expected, select_month(8) + end + + def test_select_month_with_numbers + expected = "<select name='date[month]'>\n" + expected << "<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8' selected=\"selected\">8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n" + expected << "</select>\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 = "<select name='date[month]'>\n" + expected << "<option value='1'>1 - January</option>\n<option value='2'>2 - February</option>\n<option value='3'>3 - March</option>\n<option value='4'>4 - April</option>\n<option value='5'>5 - May</option>\n<option value='6'>6 - June</option>\n<option value='7'>7 - July</option>\n<option value='8' selected=\"selected\">8 - August</option>\n<option value='9'>9 - September</option>\n<option value='10'>10 - October</option>\n<option value='11'>11 - November</option>\n<option value='12'>12 - December</option>\n" + expected << "</select>\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_year + expected = "<select name='date[year]'>\n" + expected << "<option selected=\"selected\">2003</option>\n<option>2004</option>\n<option>2005</option>\n" + expected << "</select>\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_type_discarding + expected = "<select name='date_year'>\n" + expected << "<option selected=\"selected\">2003</option>\n<option>2004</option>\n<option>2005</option>\n" + expected << "</select>\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_date + expected = "<select name='date[first][year]'>\n" + expected << "<option selected=\"selected\">2003</option>\n<option>2004</option>\n<option>2005</option>\n" + expected << "</select>\n" + + expected << "<select name='date[first][month]'>\n" + expected << "<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option value='6'>June</option>\n<option value='7'>July</option>\n<option value='8' selected=\"selected\">August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n" + expected << "</select>\n" + + expected << "<select name='date[first][day]'>\n" + expected << +"<option>1</option>\n<option>2</option>\n<option>3</option>\n<option>4</option>\n<option>5</option>\n<option>6</option>\n<option>7</option>\n<option>8</option>\n<option>9</option>\n<option>10</option>\n<option>11</option>\n<option>12</option>\n<option>13</option>\n<option>14</option>\n<option>15</option>\n<option selected=\"selected\">16</option>\n<option>17</option>\n<option>18</option>\n<option>19</option>\n<option>20</option>\n<option>21</option>\n<option>22</option>\n<option>23</option>\n<option>24</option>\n<option>25</option>\n<option>26</option>\n<option>27</option>\n<option>28</option>\n<option>29</option>\n<option>30</option>\n<option>31</option>\n" + expected << "</select>\n" + + assert_equal expected, select_date( + Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]" + ) + end +end diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb new file mode 100644 index 0000000000..8f3d5ebb94 --- /dev/null +++ b/actionpack/test/template/form_helper_test.rb @@ -0,0 +1,124 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/form_helper' + +class FormHelperTest < Test::Unit::TestCase + include ActionView::Helpers::FormHelper + + old_verbose, $VERBOSE = $VERBOSE, nil + Post = Struct.new("Post", :title, :author_name, :body, :secret, :written_on) + $VERBOSE = old_verbose + + def setup + @post = Post.new + def @post.errors() Class.new{ def on(field) field == "author_name" end }.new 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) + end + + def test_text_field + assert_equal( + '<input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />', text_field("post", "title") + ) + assert_equal( + '<input id="post_title" name="post[title]" size="30" type="password" value="Hello World" />', password_field("post", "title") + ) + assert_equal( + '<input id="person_name" name="person[name]" size="30" type="password" value="" />', password_field("person", "name") + ) + end + + def test_text_field_with_escapes + @post.title = "<b>Hello World</b>" + assert_equal( + '<input id="post_title" name="post[title]" size="30" type="text" value="<b>Hello World</b>" />', text_field("post", "title") + ) + end + + def test_text_field_with_options + assert_equal( + '<input id="post_title" name="post[title]" size="35" type="text" value="Hello World" />', + text_field("post", "title", "size" => "35") + ) + end + + def test_text_field_assuming_size + assert_equal( + '<input id="post_title" maxlength="35" name="post[title]" size="35" type="text" value="Hello World" />', + text_field("post", "title", "maxlength" => 35) + ) + end + + def test_check_box + assert_equal( + '<input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" /><input name="post[secret]" type="hidden" value="0" />', + check_box("post", "secret") + ) + + @post.secret = 0 + assert_equal( + '<input id="post_secret" name="post[secret]" type="checkbox" value="1" /><input name="post[secret]" type="hidden" value="0" />', + check_box("post", "secret") + ) + + @post.secret = true + assert_equal( + '<input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" /><input name="post[secret]" type="hidden" value="0" />', + check_box("post", "secret") + ) + end + + def test_text_area + assert_equal( + '<textarea cols="40" id="post_body" name="post[body]" rows="20" wrap="virtual">Back to the hill and over it again!</textarea>', + text_area("post", "body") + ) + end + + def test_text_area_with_escapes + @post.body = "Back to <i>the</i> hill and over it again!" + assert_equal( + '<textarea cols="40" id="post_body" name="post[body]" rows="20" wrap="virtual">Back to <i>the</i> hill and over it again!</textarea>', + text_area("post", "body") + ) + end + + def test_date_selects + assert_equal( + '<textarea cols="40" id="post_body" name="post[body]" rows="20" wrap="virtual">Back to the hill and over it again!</textarea>', + text_area("post", "body") + ) + end + + + def test_explicit_name + assert_equal( + '<input id="post_title" name="dont guess" size="30" type="text" value="Hello World" />', text_field("post", "title", "name" => "dont guess") + ) + assert_equal( + '<textarea cols="40" id="post_body" name="really!" rows="20" wrap="virtual">Back to the hill and over it again!</textarea>', + text_area("post", "body", "name" => "really!") + ) + assert_equal( + '<input checked="checked" id="post_secret" name="i mean it" type="checkbox" value="1" /><input name="i mean it" type="hidden" value="0" />', + check_box("post", "secret", "name" => "i mean it") + ) + end + + def test_explicit_id + assert_equal( + '<input id="dont guess" name="post[title]" size="30" type="text" value="Hello World" />', text_field("post", "title", "id" => "dont guess") + ) + assert_equal( + '<textarea cols="40" id="really!" name="post[body]" rows="20" wrap="virtual">Back to the hill and over it again!</textarea>', + text_area("post", "body", "id" => "really!") + ) + assert_equal( + '<input checked="checked" id="i mean it" name="post[secret]" type="checkbox" value="1" /><input name="post[secret]" type="hidden" value="0" />', + check_box("post", "secret", "id" => "i mean it") + ) + end +end diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb new file mode 100644 index 0000000000..fa0a37aa36 --- /dev/null +++ b/actionpack/test/template/form_options_helper_test.rb @@ -0,0 +1,165 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/form_options_helper' + +class FormOptionsHelperTest < Test::Unit::TestCase + include ActionView::Helpers::FormOptionsHelper + + old_verbose, $VERBOSE = $VERBOSE, nil + 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) + $VERBOSE = old_verbose + + def test_collection_options + @posts = [ + Post.new("<Abe> went home", "<Abe>", "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_equal( + "<option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>", + options_from_collection_for_select(@posts, "author_name", "title") + ) + end + + + def test_collection_options_with_preselected_value + @posts = [ + Post.new("<Abe> went home", "<Abe>", "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_equal( + "<option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>", + options_from_collection_for_select(@posts, "author_name", "title", "Babe") + ) + end + + def test_collection_options_with_preselected_value_array + @posts = [ + Post.new("<Abe> went home", "<Abe>", "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_equal( + "<option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\" selected=\"selected\">Cabe went home</option>", + options_from_collection_for_select(@posts, "author_name", "title", [ "Babe", "Cabe" ]) + ) + end + + def test_array_options_for_select + assert_equal( + "<option><Denmark></option>\n<option>USA</option>\n<option>Sweden</option>", + options_for_select([ "<Denmark>", "USA", "Sweden" ]) + ) + end + + def test_array_options_for_select_with_selection + assert_equal( + "<option>Denmark</option>\n<option selected=\"selected\"><USA></option>\n<option>Sweden</option>", + options_for_select([ "Denmark", "<USA>", "Sweden" ], "<USA>") + ) + end + + def test_array_options_for_select_with_selection_array + assert_equal( + "<option>Denmark</option>\n<option selected=\"selected\"><USA></option>\n<option selected=\"selected\">Sweden</option>", + options_for_select([ "Denmark", "<USA>", "Sweden" ], [ "<USA>", "Sweden" ]) + ) + end + + def test_hash_options_for_select + assert_equal( + "<option value=\"<Kroner>\"><DKR></option>\n<option value=\"Dollar\">$</option>", + options_for_select({ "$" => "Dollar", "<DKR>" => "<Kroner>" }) + ) + end + + def test_hash_options_for_select_with_selection + assert_equal( + "<option value=\"<Kroner>\"><DKR></option>\n<option value=\"Dollar\" selected=\"selected\">$</option>", + options_for_select({ "$" => "Dollar", "<DKR>" => "<Kroner>" }, "Dollar") + ) + end + + def test_hash_options_for_select_with_selection + assert_equal( + "<option value=\"<Kroner>\" selected=\"selected\"><DKR></option>\n<option value=\"Dollar\" selected=\"selected\">$</option>", + options_for_select({ "$" => "Dollar", "<DKR>" => "<Kroner>" }, [ "Dollar", "<Kroner>" ]) + ) + end + + def test_html_option_groups_from_collection + @continents = [ + Continent.new("<Africa>", [Country.new("<sa>", "<South Africa>"), Country.new("so", "Somalia")] ), + Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")] ) + ] + + assert_equal( + "<optgroup label=\"<Africa>\"><option value=\"<sa>\"><South Africa></option>\n<option value=\"so\">Somalia</option></optgroup><optgroup label=\"Europe\"><option value=\"dk\" selected=\"selected\">Denmark</option>\n<option value=\"ie\">Ireland</option></optgroup>", + option_groups_from_collection_for_select(@continents, "countries", "continent_name", "country_id", "country_name", "dk") + ) + end + + def test_select + @post = Post.new + @post.category = "<mus>" + assert_equal( + "<select id=\"post_category\" name=\"post[category]\"><option>abe</option>\n<option selected=\"selected\"><mus></option>\n<option>hest</option></select>", + select("post", "category", %w( abe <mus> hest)) + ) + end + + def test_select_with_blank + @post = Post.new + @post.category = "<mus>" + assert_equal( + "<select id=\"post_category\" name=\"post[category]\"><option></option>\n<option>abe</option>\n<option selected=\"selected\"><mus></option>\n<option>hest</option></select>", + select("post", "category", %w( abe <mus> hest), :include_blank => true) + ) + end + + def test_collection_select + @posts = [ + Post.new("<Abe> went home", "<Abe>", "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_equal( + "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"<Abe>\"><Abe></option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>", + collection_select("post", "author_name", @posts, "author_name", "author_name") + ) + end + + def test_collection_select_with_blank_and_style + @posts = [ + Post.new("<Abe> went home", "<Abe>", "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_equal( + "<select id=\"post_author_name\" name=\"post[author_name]\" style=\"width: 200px\"><option></option>\n<option value=\"<Abe>\"><Abe></option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>", + 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_equal( + "<select id=\"post_origin\" name=\"post[origin]\"><option>Albania</option>\n<option>Algeria</option>\n<option>American Samoa</option>\n<option>Andorra</option>\n<option>Angola</option>\n<option>Anguilla</option>\n<option>Antarctica</option>\n<option>Antigua And Barbuda</option>\n<option>Argentina</option>\n<option>Armenia</option>\n<option>Aruba</option>\n<option>Australia</option>\n<option>Austria</option>\n<option>Azerbaijan</option>\n<option>Bahamas</option>\n<option>Bahrain</option>\n<option>Bangladesh</option>\n<option>Barbados</option>\n<option>Belarus</option>\n<option>Belgium</option>\n<option>Belize</option>\n<option>Benin</option>\n<option>Bermuda</option>\n<option>Bhutan</option>\n<option>Bolivia</option>\n<option>Bosnia and Herzegowina</option>\n<option>Botswana</option>\n<option>Bouvet Island</option>\n<option>Brazil</option>\n<option>British Indian Ocean Territory</option>\n<option>Brunei Darussalam</option>\n<option>Bulgaria</option>\n<option>Burkina Faso</option>\n<option>Burma</option>\n<option>Burundi</option>\n<option>Cambodia</option>\n<option>Cameroon</option>\n<option>Canada</option>\n<option>Cape Verde</option>\n<option>Cayman Islands</option>\n<option>Central African Republic</option>\n<option>Chad</option>\n<option>Chile</option>\n<option>China</option>\n<option>Christmas Island</option>\n<option>Cocos (Keeling) Islands</option>\n<option>Colombia</option>\n<option>Comoros</option>\n<option>Congo</option>\n<option>Congo, the Democratic Republic of the</option>\n<option>Cook Islands</option>\n<option>Costa Rica</option>\n<option>Cote d'Ivoire</option>\n<option>Croatia</option>\n<option>Cyprus</option>\n<option>Czech Republic</option>\n<option selected=\"selected\">Denmark</option>\n<option>Djibouti</option>\n<option>Dominica</option>\n<option>Dominican Republic</option>\n<option>East Timor</option>\n<option>Ecuador</option>\n<option>Egypt</option>\n<option>El Salvador</option>\n<option>England</option>\n<option>Equatorial Guinea</option>\n<option>Eritrea</option>\n<option>Espana</option>\n<option>Estonia</option>\n<option>Ethiopia</option>\n<option>Falkland Islands</option>\n<option>Faroe Islands</option>\n<option>Fiji</option>\n<option>Finland</option>\n<option>France</option>\n<option>French Guiana</option>\n<option>French Polynesia</option>\n<option>French Southern Territories</option>\n<option>Gabon</option>\n<option>Gambia</option>\n<option>Georgia</option>\n<option>Germany</option>\n<option>Ghana</option>\n<option>Gibraltar</option>\n<option>Great Britain</option>\n<option>Greece</option>\n<option>Greenland</option>\n<option>Grenada</option>\n<option>Guadeloupe</option>\n<option>Guam</option>\n<option>Guatemala</option>\n<option>Guinea</option>\n<option>Guinea-Bissau</option>\n<option>Guyana</option>\n<option>Haiti</option>\n<option>Heard and Mc Donald Islands</option>\n<option>Honduras</option>\n<option>Hong Kong</option>\n<option>Hungary</option>\n<option>Iceland</option>\n<option>India</option>\n<option>Indonesia</option>\n<option>Ireland</option>\n<option>Israel</option>\n<option>Italy</option>\n<option>Jamaica</option>\n<option>Japan</option>\n<option>Jordan</option>\n<option>Kazakhstan</option>\n<option>Kenya</option>\n<option>Kiribati</option>\n<option>Korea, Republic of</option>\n<option>Korea (South)</option>\n<option>Kuwait</option>\n<option>Kyrgyzstan</option>\n<option>Lao People's Democratic Republic</option>\n<option>Latvia</option>\n<option>Lebanon</option>\n<option>Lesotho</option>\n<option>Liberia</option>\n<option>Liechtenstein</option>\n<option>Lithuania</option>\n<option>Luxembourg</option>\n<option>Macau</option>\n<option>Macedonia</option>\n<option>Madagascar</option>\n<option>Malawi</option>\n<option>Malaysia</option>\n<option>Maldives</option>\n<option>Mali</option>\n<option>Malta</option>\n<option>Marshall Islands</option>\n<option>Martinique</option>\n<option>Mauritania</option>\n<option>Mauritius</option>\n<option>Mayotte</option>\n<option>Mexico</option>\n<option>Micronesia, Federated States of</option>\n<option>Moldova, Republic of</option>\n<option>Monaco</option>\n<option>Mongolia</option>\n<option>Montserrat</option>\n<option>Morocco</option>\n<option>Mozambique</option>\n<option>Myanmar</option>\n<option>Namibia</option>\n<option>Nauru</option>\n<option>Nepal</option>\n<option>Netherlands</option>\n<option>Netherlands Antilles</option>\n<option>New Caledonia</option>\n<option>New Zealand</option>\n<option>Nicaragua</option>\n<option>Niger</option>\n<option>Nigeria</option>\n<option>Niue</option>\n<option>Norfolk Island</option>\n<option>Northern Ireland</option>\n<option>Northern Mariana Islands</option>\n<option>Norway</option>\n<option>Oman</option>\n<option>Pakistan</option>\n<option>Palau</option>\n<option>Panama</option>\n<option>Papua New Guinea</option>\n<option>Paraguay</option>\n<option>Peru</option>\n<option>Philippines</option>\n<option>Pitcairn</option>\n<option>Poland</option>\n<option>Portugal</option>\n<option>Puerto Rico</option>\n<option>Qatar</option>\n<option>Reunion</option>\n<option>Romania</option>\n<option>Russia</option>\n<option>Rwanda</option>\n<option>Saint Kitts and Nevis</option>\n<option>Saint Lucia</option>\n<option>Saint Vincent and the Grenadines</option>\n<option>Samoa (Independent)</option>\n<option>San Marino</option>\n<option>Sao Tome and Principe</option>\n<option>Saudi Arabia</option>\n<option>Scotland</option>\n<option>Senegal</option>\n<option>Seychelles</option>\n<option>Sierra Leone</option>\n<option>Singapore</option>\n<option>Slovakia</option>\n<option>Slovenia</option>\n<option>Solomon Islands</option>\n<option>Somalia</option>\n<option>South Africa</option>\n<option>South Georgia and the South Sandwich Islands</option>\n<option>South Korea</option>\n<option>Spain</option>\n<option>Sri Lanka</option>\n<option>St. Helena</option>\n<option>St. Pierre and Miquelon</option>\n<option>Suriname</option>\n<option>Svalbard and Jan Mayen Islands</option>\n<option>Swaziland</option>\n<option>Sweden</option>\n<option>Switzerland</option>\n<option>Taiwan</option>\n<option>Tajikistan</option>\n<option>Tanzania</option>\n<option>Thailand</option>\n<option>Togo</option>\n<option>Tokelau</option>\n<option>Tonga</option>\n<option>Trinidad</option>\n<option>Trinidad and Tobago</option>\n<option>Tunisia</option>\n<option>Turkey</option>\n<option>Turkmenistan</option>\n<option>Turks and Caicos Islands</option>\n<option>Tuvalu</option>\n<option>Uganda</option>\n<option>Ukraine</option>\n<option>United Arab Emirates</option>\n<option>United Kingdom</option>\n<option>United States</option>\n<option>United States Minor Outlying Islands</option>\n<option>Uruguay</option>\n<option>Uzbekistan</option>\n<option>Vanuatu</option>\n<option>Vatican City State (Holy See)</option>\n<option>Venezuela</option>\n<option>Viet Nam</option>\n<option>Virgin Islands (British)</option>\n<option>Virgin Islands (U.S.)</option>\n<option>Wales</option>\n<option>Wallis and Futuna Islands</option>\n<option>Western Sahara</option>\n<option>Yemen</option>\n<option>Zambia</option>\n<option>Zimbabwe</option></select>", + country_select("post", "origin") + ) + end +end diff --git a/actionpack/test/template/tag_helper_test.rb b/actionpack/test/template/tag_helper_test.rb new file mode 100644 index 0000000000..c3289af50c --- /dev/null +++ b/actionpack/test/template/tag_helper_test.rb @@ -0,0 +1,18 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/tag_helper' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/url_helper' + +class TagHelperTest < Test::Unit::TestCase + include ActionView::Helpers::TagHelper + include ActionView::Helpers::UrlHelper + + def test_tag + assert_equal "<p class=\"show\" />", tag("p", "class" => "show") + end + + def test_content_tag + assert_equal "<a href=\"create\">Create</a>", content_tag("a", "Create", "href" => "create") + end + + # FIXME: Test form tag +end
\ No newline at end of file diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb new file mode 100644 index 0000000000..347420a72b --- /dev/null +++ b/actionpack/test/template/text_helper_test.rb @@ -0,0 +1,62 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/text_helper' + +class TextHelperTest < Test::Unit::TestCase + include ActionView::Helpers::TextHelper + + def test_truncate + assert_equal "Hello World!", truncate("Hello World!", 12) + assert_equal "Hello Worl...", truncate("Hello World!!", 12) + end + + def test_strip_links + assert_equal "on my mind", strip_links("<a href='almost'>on my mind</a>") + end + + def test_highlighter + assert_equal( + "This is a <strong class=\"highlight\">beautiful</strong> morning", + highlight("This is a beautiful morning", "beautiful") + ) + + assert_equal( + "This is a <strong class=\"highlight\">beautiful</strong> morning, but also a <strong class=\"highlight\">beautiful</strong> day", + highlight("This is a beautiful morning, but also a beautiful day", "beautiful") + ) + + assert_equal( + "This is a <b>beautiful</b> morning, but also a <b>beautiful</b> day", + highlight("This is a beautiful morning, but also a beautiful day", "beautiful", '<b>\1</b>') + ) + end + + def test_highlighter_with_regexp + assert_equal( + "This is a <strong class=\"highlight\">beautiful!</strong> morning", + highlight("This is a beautiful! morning", "beautiful!") + ) + + assert_equal( + "This is a <strong class=\"highlight\">beautiful! morning</strong>", + highlight("This is a beautiful! morning", "beautiful! morning") + ) + + assert_equal( + "This is a <strong class=\"highlight\">beautiful? morning</strong>", + highlight("This is a beautiful? morning", "beautiful? morning") + ) + end + + def test_excerpt + assert_equal("...is a beautiful morni...", excerpt("This is a beautiful morning", "beautiful", 5)) + assert_equal("This is a...", excerpt("This is a beautiful morning", "this", 5)) + assert_equal("...iful morning", excerpt("This is a beautiful morning", "morning", 5)) + assert_equal("...iful morning", excerpt("This is a beautiful morning", "morning", 5)) + assert_nil excerpt("This is a beautiful morning", "day") + end + + def test_pluralization + assert_equal("1 count", pluralize(1, "count")) + assert_equal("2 counts", pluralize(2, "count")) + end +end
\ No newline at end of file diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb new file mode 100644 index 0000000000..198b26b113 --- /dev/null +++ b/actionpack/test/template/url_helper_test.rb @@ -0,0 +1,49 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/url_helper' +require File.dirname(__FILE__) + '/../../lib/action_view/helpers/tag_helper' + +class UrlHelperTest < Test::Unit::TestCase + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::TagHelper + + def setup + @controller = Class.new do + def url_for(options, *parameters_for_method_reference) + "http://www.world.com" + end + end + @controller = @controller.new + end + + # todo: missing test cases + def test_link_tag_with_straight_url + assert_equal "<a href=\"http://www.world.com\">Hello</a>", link_to("Hello", "http://www.world.com") + end + + def test_link_tag_with_javascript_confirm + assert_equal( + "<a href=\"http://www.world.com\" onclick=\"return confirm('Are you sure?');\">Hello</a>", + link_to("Hello", "http://www.world.com", :confirm => "Are you sure?") + ) + end + + def test_link_unless_current + @params = { "controller" => "weblog", "action" => "show"} + assert_equal "Showing", link_to_unless_current("Showing", :action => "show", :controller => "weblog") + assert "<a href=\"http://www.world.com\">Listing</a>", link_to_unless_current("Listing", :action => "list", :controller => "weblog") + end + + def test_mail_to + assert_equal "<a href=\"mailto:david@loudthinking.com\">david@loudthinking.com</a>", mail_to("david@loudthinking.com") + assert_equal "<a href=\"mailto:david@loudthinking.com\">David Heinemeier Hansson</a>", mail_to("david@loudthinking.com", "David Heinemeier Hansson") + assert_equal( + "<a class=\"admin\" href=\"mailto:david@loudthinking.com\">David Heinemeier Hansson</a>", + mail_to("david@loudthinking.com", "David Heinemeier Hansson", "class" => "admin") + ) + end + + def test_link_with_nil_html_options + assert "<a href=\"http://www.world.com\">Hello</a>", + link_to("Hello", {:action => 'myaction'}, nil) + end +end
\ No newline at end of file |