Upgrade to Rails 2.2.0

As a side benefit, fix an (non-user-visible) bug in display_s5().
Also fixed a bug where removing orphaned pages did not expire cached summary pages.
This commit is contained in:
Jacques Distler 2008-10-27 01:47:01 -05:00
parent 39348c65c2
commit 7600aef48b
827 changed files with 123652 additions and 11027 deletions

View file

@ -11,4 +11,8 @@ class WebSweeper < ActionController::Caching::Sweeper
web.pages.each { |page| expire_cached_page(web, page.name) } web.pages.each { |page| expire_cached_page(web, page.name) }
expire_cached_summary_pages(web) expire_cached_summary_pages(web)
end end
def after_remove_orphaned_pages(web)
expire_cached_summary_pages(web)
end
end end

View file

@ -410,7 +410,7 @@ class WikiController < ApplicationController
def remote_ip def remote_ip
ip = request.remote_ip ip = request.remote_ip
logger.info(ip) logger.info(ip)
ip.gsub!(Regexp.union(Resolv::IPv4::Regex, Resolv::IPv6::Regex), '\0') || 'bogus address' ip.dup.gsub!(Regexp.union(Resolv::IPv4::Regex, Resolv::IPv6::Regex), '\0') || 'bogus address'
end end
def render_atom(hide_description = false, limit = 15) def render_atom(hide_description = false, limit = 15)

View file

@ -13,7 +13,7 @@
<ul> <ul>
<% @pages_in_category.each do |page| %> <% @pages_in_category.each do |page| %>
<li> <li>
<%= link_to_existing_page page, truncate(page.plain_name, 35) %> <%= link_to_existing_page page, truncate(page.plain_name, :length => 35) %>
</li> </li>
<% end %></ul> <% end %></ul>
@ -36,7 +36,7 @@
<ul style="margin-bottom: 10px"> <ul style="margin-bottom: 10px">
<% @page_names_that_are_wanted.each do |wanted_page_name| %> <% @page_names_that_are_wanted.each do |wanted_page_name| %>
<li> <li>
<%= link_to_page(wanted_page_name, @web, truncate(WikiWords.separate(wanted_page_name), 35)) %> <%= link_to_page(wanted_page_name, @web, truncate(WikiWords.separate(wanted_page_name), :length => 35)) %>
wanted by wanted by
<%= @web.select.pages_that_reference(wanted_page_name).collect { |referring_page| <%= @web.select.pages_that_reference(wanted_page_name).collect { |referring_page|
link_to_existing_page referring_page link_to_existing_page referring_page
@ -56,7 +56,7 @@
<ul style="margin-bottom: 35px"> <ul style="margin-bottom: 35px">
<% @pages_that_are_orphaned.each do |orphan_page| %> <% @pages_that_are_orphaned.each do |orphan_page| %>
<li> <li>
<%= link_to_existing_page orphan_page, truncate(orphan_page.plain_name, 35) %> <%= link_to_existing_page orphan_page, truncate(orphan_page.plain_name, :length => 35) %>
</li> </li>
<% end %> <% end %>
</ul> </ul>

View file

@ -71,8 +71,8 @@ class PageRenderer
# Renders an S5 slideshow # Renders an S5 slideshow
def display_s5 def display_s5
@display_s5 ||= render(:mode => :s5, @display_s5 ||= render(:mode => :s5,
:engine_opts => { :author => @revision.page.author, :engine_opts => {:author => @author, :title => @plain_name},
:title => @revision.page.plain_name}, :renderer => self) :renderer => self)
end end
# Returns an array of all the WikiIncludes present in the content of this revision. # Returns an array of all the WikiIncludes present in the content of this revision.

View file

@ -1,5 +1,5 @@
/* Prototype JavaScript framework, version 1.6.0.1 /* Prototype JavaScript framework, version 1.6.0.2
* (c) 2005-2007 Sam Stephenson * (c) 2005-2008 Sam Stephenson
* *
* Prototype is freely distributable under the terms of an MIT-style license. * Prototype is freely distributable under the terms of an MIT-style license.
* For details, see the Prototype web site: http://www.prototypejs.org/ * For details, see the Prototype web site: http://www.prototypejs.org/
@ -7,7 +7,7 @@
*--------------------------------------------------------------------------*/ *--------------------------------------------------------------------------*/
var Prototype = { var Prototype = {
Version: '1.6.0.1', Version: '1.6.0.2',
Browser: { Browser: {
IE: !!(window.attachEvent && !window.opera), IE: !!(window.attachEvent && !window.opera),
@ -110,7 +110,7 @@ Object.extend(Object, {
try { try {
if (Object.isUndefined(object)) return 'undefined'; if (Object.isUndefined(object)) return 'undefined';
if (object === null) return 'null'; if (object === null) return 'null';
return object.inspect ? object.inspect() : object.toString(); return object.inspect ? object.inspect() : String(object);
} catch (e) { } catch (e) {
if (e instanceof RangeError) return '...'; if (e instanceof RangeError) return '...';
throw e; throw e;
@ -171,7 +171,8 @@ Object.extend(Object, {
}, },
isArray: function(object) { isArray: function(object) {
return object && object.constructor === Array; return object != null && typeof object == "object" &&
'splice' in object && 'join' in object;
}, },
isHash: function(object) { isHash: function(object) {
@ -578,7 +579,7 @@ var Template = Class.create({
} }
return before + String.interpret(ctx); return before + String.interpret(ctx);
}.bind(this)); });
} }
}); });
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
@ -806,20 +807,20 @@ Object.extend(Enumerable, {
function $A(iterable) { function $A(iterable) {
if (!iterable) return []; if (!iterable) return [];
if (iterable.toArray) return iterable.toArray(); if (iterable.toArray) return iterable.toArray();
var length = iterable.length, results = new Array(length); var length = iterable.length || 0, results = new Array(length);
while (length--) results[length] = iterable[length]; while (length--) results[length] = iterable[length];
return results; return results;
} }
if (Prototype.Browser.WebKit) { if (Prototype.Browser.WebKit) {
function $A(iterable) { $A = function(iterable) {
if (!iterable) return []; if (!iterable) return [];
if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') && if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') &&
iterable.toArray) return iterable.toArray(); iterable.toArray) return iterable.toArray();
var length = iterable.length, results = new Array(length); var length = iterable.length || 0, results = new Array(length);
while (length--) results[length] = iterable[length]; while (length--) results[length] = iterable[length];
return results; return results;
} };
} }
Array.from = $A; Array.from = $A;
@ -1298,7 +1299,7 @@ Ajax.Request = Class.create(Ajax.Base, {
var contentType = response.getHeader('Content-type'); var contentType = response.getHeader('Content-type');
if (this.options.evalJS == 'force' if (this.options.evalJS == 'force'
|| (this.options.evalJS && contentType || (this.options.evalJS && this.isSameOrigin() && contentType
&& contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
this.evalResponse(); this.evalResponse();
} }
@ -1316,9 +1317,18 @@ Ajax.Request = Class.create(Ajax.Base, {
} }
}, },
isSameOrigin: function() {
var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
protocol: location.protocol,
domain: document.domain,
port: location.port ? ':' + location.port : ''
}));
},
getHeader: function(name) { getHeader: function(name) {
try { try {
return this.transport.getResponseHeader(name); return this.transport.getResponseHeader(name) || null;
} catch (e) { return null } } catch (e) { return null }
}, },
@ -1391,7 +1401,8 @@ Ajax.Response = Class.create({
if (!json) return null; if (!json) return null;
json = decodeURIComponent(escape(json)); json = decodeURIComponent(escape(json));
try { try {
return json.evalJSON(this.request.options.sanitizeJSON); return json.evalJSON(this.request.options.sanitizeJSON ||
!this.request.isSameOrigin());
} catch (e) { } catch (e) {
this.request.dispatchException(e); this.request.dispatchException(e);
} }
@ -1404,7 +1415,8 @@ Ajax.Response = Class.create({
this.responseText.blank()) this.responseText.blank())
return null; return null;
try { try {
return this.responseText.evalJSON(options.sanitizeJSON); return this.responseText.evalJSON(options.sanitizeJSON ||
!this.request.isSameOrigin());
} catch (e) { } catch (e) {
this.request.dispatchException(e); this.request.dispatchException(e);
} }
@ -1608,24 +1620,28 @@ Element.Methods = {
Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
insertions = {bottom:insertions}; insertions = {bottom:insertions};
var content, t, range; var content, insert, tagName, childNodes;
for (position in insertions) { for (var position in insertions) {
content = insertions[position]; content = insertions[position];
position = position.toLowerCase(); position = position.toLowerCase();
t = Element._insertionTranslations[position]; insert = Element._insertionTranslations[position];
if (content && content.toElement) content = content.toElement(); if (content && content.toElement) content = content.toElement();
if (Object.isElement(content)) { if (Object.isElement(content)) {
t.insert(element, content); insert(element, content);
continue; continue;
} }
content = Object.toHTML(content); content = Object.toHTML(content);
range = element.ownerDocument.createRange(); tagName = ((position == 'before' || position == 'after')
t.initializeRange(element, range); ? element.parentNode : element).tagName.toUpperCase();
t.insert(element, range.createContextualFragment(content.stripScripts()));
childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
if (position == 'top' || position == 'after') childNodes.reverse();
childNodes.each(insert.curry(element));
content.evalScripts.bind(content).defer(); content.evalScripts.bind(content).defer();
} }
@ -1670,7 +1686,7 @@ Element.Methods = {
}, },
descendants: function(element) { descendants: function(element) {
return $(element).getElementsBySelector("*"); return $(element).select("*");
}, },
firstDescendant: function(element) { firstDescendant: function(element) {
@ -1709,32 +1725,31 @@ Element.Methods = {
element = $(element); element = $(element);
if (arguments.length == 1) return $(element.parentNode); if (arguments.length == 1) return $(element.parentNode);
var ancestors = element.ancestors(); var ancestors = element.ancestors();
return expression ? Selector.findElement(ancestors, expression, index) : return Object.isNumber(expression) ? ancestors[expression] :
ancestors[index || 0]; Selector.findElement(ancestors, expression, index);
}, },
down: function(element, expression, index) { down: function(element, expression, index) {
element = $(element); element = $(element);
if (arguments.length == 1) return element.firstDescendant(); if (arguments.length == 1) return element.firstDescendant();
var descendants = element.descendants(); return Object.isNumber(expression) ? element.descendants()[expression] :
return expression ? Selector.findElement(descendants, expression, index) : element.select(expression)[index || 0];
descendants[index || 0];
}, },
previous: function(element, expression, index) { previous: function(element, expression, index) {
element = $(element); element = $(element);
if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
var previousSiblings = element.previousSiblings(); var previousSiblings = element.previousSiblings();
return expression ? Selector.findElement(previousSiblings, expression, index) : return Object.isNumber(expression) ? previousSiblings[expression] :
previousSiblings[index || 0]; Selector.findElement(previousSiblings, expression, index);
}, },
next: function(element, expression, index) { next: function(element, expression, index) {
element = $(element); element = $(element);
if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
var nextSiblings = element.nextSiblings(); var nextSiblings = element.nextSiblings();
return expression ? Selector.findElement(nextSiblings, expression, index) : return Object.isNumber(expression) ? nextSiblings[expression] :
nextSiblings[index || 0]; Selector.findElement(nextSiblings, expression, index);
}, },
select: function() { select: function() {
@ -1860,7 +1875,8 @@ Element.Methods = {
do { ancestor = ancestor.parentNode; } do { ancestor = ancestor.parentNode; }
while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode); while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode);
} }
if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex); if (nextAncestor && nextAncestor.sourceIndex)
return (e > a && e < nextAncestor.sourceIndex);
} }
while (element = element.parentNode) while (element = element.parentNode)
@ -2004,7 +2020,7 @@ Element.Methods = {
if (element) { if (element) {
if (element.tagName == 'BODY') break; if (element.tagName == 'BODY') break;
var p = Element.getStyle(element, 'position'); var p = Element.getStyle(element, 'position');
if (p == 'relative' || p == 'absolute') break; if (p !== 'static') break;
} }
} while (element); } while (element);
return Element._returnOffset(valueL, valueT); return Element._returnOffset(valueL, valueT);
@ -2153,46 +2169,6 @@ Element._attributeTranslations = {
} }
}; };
if (!document.createRange || Prototype.Browser.Opera) {
Element.Methods.insert = function(element, insertions) {
element = $(element);
if (Object.isString(insertions) || Object.isNumber(insertions) ||
Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
insertions = { bottom: insertions };
var t = Element._insertionTranslations, content, position, pos, tagName;
for (position in insertions) {
content = insertions[position];
position = position.toLowerCase();
pos = t[position];
if (content && content.toElement) content = content.toElement();
if (Object.isElement(content)) {
pos.insert(element, content);
continue;
}
content = Object.toHTML(content);
tagName = ((position == 'before' || position == 'after')
? element.parentNode : element).tagName.toUpperCase();
if (t.tags[tagName]) {
var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
if (position == 'top' || position == 'after') fragments.reverse();
fragments.each(pos.insert.curry(element));
}
else element.insertAdjacentHTML(pos.adjacency, content.stripScripts());
content.evalScripts.bind(content).defer();
}
return element;
};
}
if (Prototype.Browser.Opera) { if (Prototype.Browser.Opera) {
Element.Methods.getStyle = Element.Methods.getStyle.wrap( Element.Methods.getStyle = Element.Methods.getStyle.wrap(
function(proceed, element, style) { function(proceed, element, style) {
@ -2237,12 +2213,31 @@ if (Prototype.Browser.Opera) {
} }
else if (Prototype.Browser.IE) { else if (Prototype.Browser.IE) {
$w('positionedOffset getOffsetParent viewportOffset').each(function(method) { // IE doesn't report offsets correctly for static elements, so we change them
// to "relative" to get the values, then change them back.
Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
function(proceed, element) {
element = $(element);
var position = element.getStyle('position');
if (position !== 'static') return proceed(element);
element.setStyle({ position: 'relative' });
var value = proceed(element);
element.setStyle({ position: position });
return value;
}
);
$w('positionedOffset viewportOffset').each(function(method) {
Element.Methods[method] = Element.Methods[method].wrap( Element.Methods[method] = Element.Methods[method].wrap(
function(proceed, element) { function(proceed, element) {
element = $(element); element = $(element);
var position = element.getStyle('position'); var position = element.getStyle('position');
if (position != 'static') return proceed(element); if (position !== 'static') return proceed(element);
// Trigger hasLayout on the offset parent so that IE6 reports
// accurate offsetTop and offsetLeft values for position: fixed.
var offsetParent = element.getOffsetParent();
if (offsetParent && offsetParent.getStyle('position') === 'fixed')
offsetParent.setStyle({ zoom: 1 });
element.setStyle({ position: 'relative' }); element.setStyle({ position: 'relative' });
var value = proceed(element); var value = proceed(element);
element.setStyle({ position: position }); element.setStyle({ position: position });
@ -2324,7 +2319,10 @@ else if (Prototype.Browser.IE) {
}; };
Element._attributeTranslations.write = { Element._attributeTranslations.write = {
names: Object.clone(Element._attributeTranslations.read.names), names: Object.extend({
cellpadding: 'cellPadding',
cellspacing: 'cellSpacing'
}, Element._attributeTranslations.read.names),
values: { values: {
checked: function(element, value) { checked: function(element, value) {
element.checked = !!value; element.checked = !!value;
@ -2444,7 +2442,7 @@ if (Prototype.Browser.IE || Prototype.Browser.Opera) {
}; };
} }
if (document.createElement('div').outerHTML) { if ('outerHTML' in document.createElement('div')) {
Element.Methods.replace = function(element, content) { Element.Methods.replace = function(element, content) {
element = $(element); element = $(element);
@ -2482,46 +2480,26 @@ Element._returnOffset = function(l, t) {
Element._getContentFromAnonymousElement = function(tagName, html) { Element._getContentFromAnonymousElement = function(tagName, html) {
var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
if (t) {
div.innerHTML = t[0] + html + t[1]; div.innerHTML = t[0] + html + t[1];
t[2].times(function() { div = div.firstChild }); t[2].times(function() { div = div.firstChild });
} else div.innerHTML = html;
return $A(div.childNodes); return $A(div.childNodes);
}; };
Element._insertionTranslations = { Element._insertionTranslations = {
before: { before: function(element, node) {
adjacency: 'beforeBegin',
insert: function(element, node) {
element.parentNode.insertBefore(node, element); element.parentNode.insertBefore(node, element);
}, },
initializeRange: function(element, range) { top: function(element, node) {
range.setStartBefore(element);
}
},
top: {
adjacency: 'afterBegin',
insert: function(element, node) {
element.insertBefore(node, element.firstChild); element.insertBefore(node, element.firstChild);
}, },
initializeRange: function(element, range) { bottom: function(element, node) {
range.selectNodeContents(element);
range.collapse(true);
}
},
bottom: {
adjacency: 'beforeEnd',
insert: function(element, node) {
element.appendChild(node); element.appendChild(node);
}
}, },
after: { after: function(element, node) {
adjacency: 'afterEnd',
insert: function(element, node) {
element.parentNode.insertBefore(node, element.nextSibling); element.parentNode.insertBefore(node, element.nextSibling);
}, },
initializeRange: function(element, range) {
range.setStartAfter(element);
}
},
tags: { tags: {
TABLE: ['<table>', '</table>', 1], TABLE: ['<table>', '</table>', 1],
TBODY: ['<table><tbody>', '</tbody></table>', 2], TBODY: ['<table><tbody>', '</tbody></table>', 2],
@ -2532,7 +2510,6 @@ Element._insertionTranslations = {
}; };
(function() { (function() {
this.bottom.initializeRange = this.top.initializeRange;
Object.extend(this.tags, { Object.extend(this.tags, {
THEAD: this.tags.TBODY, THEAD: this.tags.TBODY,
TFOOT: this.tags.TBODY, TFOOT: this.tags.TBODY,
@ -2716,7 +2693,7 @@ document.viewport = {
window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
} }
}; };
/* Portions of the Selector class are derived from Jack Slocums DomQuery, /* Portions of the Selector class are derived from Jack Slocumâs DomQuery,
* part of YUI-Ext version 0.40, distributed under the terms of an MIT-style * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
* license. Please see http://www.yui-ext.com/ for more information. */ * license. Please see http://www.yui-ext.com/ for more information. */
@ -2962,10 +2939,10 @@ Object.extend(Selector, {
tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;',
className: 'n = h.className(n, r, "#{1}", c); c = false;', className: 'n = h.className(n, r, "#{1}", c); c = false;',
id: 'n = h.id(n, r, "#{1}", c); c = false;', id: 'n = h.id(n, r, "#{1}", c); c = false;',
attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;', attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
attr: function(m) { attr: function(m) {
m[3] = (m[5] || m[6]); m[3] = (m[5] || m[6]);
return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m); return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
}, },
pseudo: function(m) { pseudo: function(m) {
if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
@ -2989,7 +2966,8 @@ Object.extend(Selector, {
tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, tagName: /^\s*(\*|[\w\-]+)(\b|$)?/,
id: /^#([\w\-\*]+)(\b|$)/, id: /^#([\w\-\*]+)(\b|$)/,
className: /^\.([\w\-\*]+)(\b|$)/, className: /^\.([\w\-\*]+)(\b|$)/,
pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/, pseudo:
/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
attrPresence: /^\[([\w]+)\]/, attrPresence: /^\[([\w]+)\]/,
attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
}, },
@ -3014,7 +2992,7 @@ Object.extend(Selector, {
attr: function(element, matches) { attr: function(element, matches) {
var nodeValue = Element.readAttribute(element, matches[1]); var nodeValue = Element.readAttribute(element, matches[1]);
return Selector.operators[matches[2]](nodeValue, matches[3]); return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
} }
}, },
@ -3029,14 +3007,15 @@ Object.extend(Selector, {
// marks an array of nodes for counting // marks an array of nodes for counting
mark: function(nodes) { mark: function(nodes) {
var _true = Prototype.emptyFunction;
for (var i = 0, node; node = nodes[i]; i++) for (var i = 0, node; node = nodes[i]; i++)
node._counted = true; node._countedByPrototype = _true;
return nodes; return nodes;
}, },
unmark: function(nodes) { unmark: function(nodes) {
for (var i = 0, node; node = nodes[i]; i++) for (var i = 0, node; node = nodes[i]; i++)
node._counted = undefined; node._countedByPrototype = undefined;
return nodes; return nodes;
}, },
@ -3044,15 +3023,15 @@ Object.extend(Selector, {
// "ofType" flag indicates whether we're indexing for nth-of-type // "ofType" flag indicates whether we're indexing for nth-of-type
// rather than nth-child // rather than nth-child
index: function(parentNode, reverse, ofType) { index: function(parentNode, reverse, ofType) {
parentNode._counted = true; parentNode._countedByPrototype = Prototype.emptyFunction;
if (reverse) { if (reverse) {
for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
var node = nodes[i]; var node = nodes[i];
if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
} }
} else { } else {
for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
} }
}, },
@ -3061,8 +3040,8 @@ Object.extend(Selector, {
if (nodes.length == 0) return nodes; if (nodes.length == 0) return nodes;
var results = [], n; var results = [], n;
for (var i = 0, l = nodes.length; i < l; i++) for (var i = 0, l = nodes.length; i < l; i++)
if (!(n = nodes[i])._counted) { if (!(n = nodes[i])._countedByPrototype) {
n._counted = true; n._countedByPrototype = Prototype.emptyFunction;
results.push(Element.extend(n)); results.push(Element.extend(n));
} }
return Selector.handlers.unmark(results); return Selector.handlers.unmark(results);
@ -3114,7 +3093,7 @@ Object.extend(Selector, {
// TOKEN FUNCTIONS // TOKEN FUNCTIONS
tagName: function(nodes, root, tagName, combinator) { tagName: function(nodes, root, tagName, combinator) {
tagName = tagName.toUpperCase(); var uTagName = tagName.toUpperCase();
var results = [], h = Selector.handlers; var results = [], h = Selector.handlers;
if (nodes) { if (nodes) {
if (combinator) { if (combinator) {
@ -3127,7 +3106,7 @@ Object.extend(Selector, {
if (tagName == "*") return nodes; if (tagName == "*") return nodes;
} }
for (var i = 0, node; node = nodes[i]; i++) for (var i = 0, node; node = nodes[i]; i++)
if (node.tagName.toUpperCase() == tagName) results.push(node); if (node.tagName.toUpperCase() === uTagName) results.push(node);
return results; return results;
} else return root.getElementsByTagName(tagName); } else return root.getElementsByTagName(tagName);
}, },
@ -3174,16 +3153,18 @@ Object.extend(Selector, {
return results; return results;
}, },
attrPresence: function(nodes, root, attr) { attrPresence: function(nodes, root, attr, combinator) {
if (!nodes) nodes = root.getElementsByTagName("*"); if (!nodes) nodes = root.getElementsByTagName("*");
if (nodes && combinator) nodes = this[combinator](nodes);
var results = []; var results = [];
for (var i = 0, node; node = nodes[i]; i++) for (var i = 0, node; node = nodes[i]; i++)
if (Element.hasAttribute(node, attr)) results.push(node); if (Element.hasAttribute(node, attr)) results.push(node);
return results; return results;
}, },
attr: function(nodes, root, attr, value, operator) { attr: function(nodes, root, attr, value, operator, combinator) {
if (!nodes) nodes = root.getElementsByTagName("*"); if (!nodes) nodes = root.getElementsByTagName("*");
if (nodes && combinator) nodes = this[combinator](nodes);
var handler = Selector.operators[operator], results = []; var handler = Selector.operators[operator], results = [];
for (var i = 0, node; node = nodes[i]; i++) { for (var i = 0, node; node = nodes[i]; i++) {
var nodeValue = Element.readAttribute(node, attr); var nodeValue = Element.readAttribute(node, attr);
@ -3262,7 +3243,7 @@ Object.extend(Selector, {
var h = Selector.handlers, results = [], indexed = [], m; var h = Selector.handlers, results = [], indexed = [], m;
h.mark(nodes); h.mark(nodes);
for (var i = 0, node; node = nodes[i]; i++) { for (var i = 0, node; node = nodes[i]; i++) {
if (!node.parentNode._counted) { if (!node.parentNode._countedByPrototype) {
h.index(node.parentNode, reverse, ofType); h.index(node.parentNode, reverse, ofType);
indexed.push(node.parentNode); indexed.push(node.parentNode);
} }
@ -3300,7 +3281,7 @@ Object.extend(Selector, {
var exclusions = new Selector(selector).findElements(root); var exclusions = new Selector(selector).findElements(root);
h.mark(exclusions); h.mark(exclusions);
for (var i = 0, results = [], node; node = nodes[i]; i++) for (var i = 0, results = [], node; node = nodes[i]; i++)
if (!node._counted) results.push(node); if (!node._countedByPrototype) results.push(node);
h.unmark(exclusions); h.unmark(exclusions);
return results; return results;
}, },
@ -3334,11 +3315,19 @@ Object.extend(Selector, {
'|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); } '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
}, },
split: function(expression) {
var expressions = [];
expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
expressions.push(m[1].strip());
});
return expressions;
},
matchElements: function(elements, expression) { matchElements: function(elements, expression) {
var matches = new Selector(expression).findElements(), h = Selector.handlers; var matches = $$(expression), h = Selector.handlers;
h.mark(matches); h.mark(matches);
for (var i = 0, results = [], element; element = elements[i]; i++) for (var i = 0, results = [], element; element = elements[i]; i++)
if (element._counted) results.push(element); if (element._countedByPrototype) results.push(element);
h.unmark(matches); h.unmark(matches);
return results; return results;
}, },
@ -3351,11 +3340,7 @@ Object.extend(Selector, {
}, },
findChildElements: function(element, expressions) { findChildElements: function(element, expressions) {
var exprs = expressions.join(','); expressions = Selector.split(expressions.join(','));
expressions = [];
exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
expressions.push(m[1].strip());
});
var results = [], h = Selector.handlers; var results = [], h = Selector.handlers;
for (var i = 0, l = expressions.length, selector; i < l; i++) { for (var i = 0, l = expressions.length, selector; i < l; i++) {
selector = new Selector(expressions[i].strip()); selector = new Selector(expressions[i].strip());
@ -3366,13 +3351,22 @@ Object.extend(Selector, {
}); });
if (Prototype.Browser.IE) { if (Prototype.Browser.IE) {
Object.extend(Selector.handlers, {
// IE returns comment nodes on getElementsByTagName("*"). // IE returns comment nodes on getElementsByTagName("*").
// Filter them out. // Filter them out.
Selector.handlers.concat = function(a, b) { concat: function(a, b) {
for (var i = 0, node; node = b[i]; i++) for (var i = 0, node; node = b[i]; i++)
if (node.tagName !== "!") a.push(node); if (node.tagName !== "!") a.push(node);
return a; return a;
}; },
// IE improperly serializes _countedByPrototype in (inner|outer)HTML.
unmark: function(nodes) {
for (var i = 0, node; node = nodes[i]; i++)
node.removeAttribute('_countedByPrototype');
return nodes;
}
});
} }
function $$() { function $$() {
@ -3850,9 +3844,9 @@ Object.extend(Event, (function() {
var cache = Event.cache; var cache = Event.cache;
function getEventID(element) { function getEventID(element) {
if (element._eventID) return element._eventID; if (element._prototypeEventID) return element._prototypeEventID[0];
arguments.callee.id = arguments.callee.id || 1; arguments.callee.id = arguments.callee.id || 1;
return element._eventID = ++arguments.callee.id; return element._prototypeEventID = [++arguments.callee.id];
} }
function getDOMEventName(eventName) { function getDOMEventName(eventName) {
@ -3880,7 +3874,7 @@ Object.extend(Event, (function() {
return false; return false;
Event.extend(event); Event.extend(event);
handler.call(element, event) handler.call(element, event);
}; };
wrapper.handler = handler; wrapper.handler = handler;
@ -3962,11 +3956,12 @@ Object.extend(Event, (function() {
if (element == document && document.createEvent && !element.dispatchEvent) if (element == document && document.createEvent && !element.dispatchEvent)
element = document.documentElement; element = document.documentElement;
var event;
if (document.createEvent) { if (document.createEvent) {
var event = document.createEvent("HTMLEvents"); event = document.createEvent("HTMLEvents");
event.initEvent("dataavailable", true, true); event.initEvent("dataavailable", true, true);
} else { } else {
var event = document.createEventObject(); event = document.createEventObject();
event.eventType = "ondataavailable"; event.eventType = "ondataavailable";
} }
@ -3995,20 +3990,21 @@ Element.addMethods({
Object.extend(document, { Object.extend(document, {
fire: Element.Methods.fire.methodize(), fire: Element.Methods.fire.methodize(),
observe: Element.Methods.observe.methodize(), observe: Element.Methods.observe.methodize(),
stopObserving: Element.Methods.stopObserving.methodize() stopObserving: Element.Methods.stopObserving.methodize(),
loaded: false
}); });
(function() { (function() {
/* Support for the DOMContentLoaded event is based on work by Dan Webb, /* Support for the DOMContentLoaded event is based on work by Dan Webb,
Matthias Miller, Dean Edwards and John Resig. */ Matthias Miller, Dean Edwards and John Resig. */
var timer, fired = false; var timer;
function fireContentLoadedEvent() { function fireContentLoadedEvent() {
if (fired) return; if (document.loaded) return;
if (timer) window.clearInterval(timer); if (timer) window.clearInterval(timer);
document.fire("dom:loaded"); document.fire("dom:loaded");
fired = true; document.loaded = true;
} }
if (document.addEventListener) { if (document.addEventListener) {

View file

@ -1,6 +1,9 @@
*2.1.1 (September 4th, 2008)* *2.2.0 [RC1] (October 24th, 2008)*
* Included in Rails 2.1.1 * Add layout functionality to mailers [Pratik]
Mailer layouts behaves just like controller layouts, except layout names need to
have '_mailer' postfix for them to be automatically picked up.
*2.1.0 (May 31st, 2008)* *2.1.0 (May 31st, 2008)*

View file

@ -5,8 +5,6 @@ require 'rake/rdoctask'
require 'rake/packagetask' require 'rake/packagetask'
require 'rake/gempackagetask' require 'rake/gempackagetask'
require 'rake/contrib/sshpublisher' require 'rake/contrib/sshpublisher'
require 'rake/contrib/rubyforgepublisher'
require File.join(File.dirname(__FILE__), 'lib', 'action_mailer', 'version') require File.join(File.dirname(__FILE__), 'lib', 'action_mailer', 'version')
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
@ -57,7 +55,7 @@ spec = Gem::Specification.new do |s|
s.rubyforge_project = "actionmailer" s.rubyforge_project = "actionmailer"
s.homepage = "http://www.rubyonrails.org" s.homepage = "http://www.rubyonrails.org"
s.add_dependency('actionpack', '= 2.1.1' + PKG_BUILD) s.add_dependency('actionpack', '= 2.2.0' + PKG_BUILD)
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'
@ -78,8 +76,8 @@ end
desc "Publish the API documentation" desc "Publish the API documentation"
task :pgem => [:package] do task :pgem => [:package] do
Rake::SshFilePublisher.new("david@greed.loudthinking.com", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
`ssh david@greed.loudthinking.com '/u/sites/gems/gemupdate.sh'` `ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'`
end end
desc "Publish the API documentation" desc "Publish the API documentation"

View file

@ -21,13 +21,13 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++ #++
unless defined?(ActionController) begin
begin require 'action_controller'
$:.unshift "#{File.dirname(__FILE__)}/../../actionpack/lib" rescue LoadError
actionpack_path = "#{File.dirname(__FILE__)}/../../actionpack/lib"
if File.directory?(actionpack_path)
$:.unshift actionpack_path
require 'action_controller' require 'action_controller'
rescue LoadError
require 'rubygems'
gem 'actionpack', '>= 1.12.5'
end end
end end

View file

@ -246,16 +246,16 @@ module ActionMailer #:nodoc:
# +implicit_parts_order+. # +implicit_parts_order+.
class Base class Base
include AdvAttrAccessor, PartContainer include AdvAttrAccessor, PartContainer
include ActionController::UrlWriter if Object.const_defined?(:ActionController) if Object.const_defined?(:ActionController)
include ActionController::UrlWriter
include ActionController::Layout
end
private_class_method :new #:nodoc: private_class_method :new #:nodoc:
class_inheritable_accessor :template_root class_inheritable_accessor :view_paths
cattr_accessor :logger cattr_accessor :logger
cattr_accessor :template_extensions
@@template_extensions = ['erb', 'builder', 'rhtml', 'rxml']
@@smtp_settings = { @@smtp_settings = {
:address => "localhost", :address => "localhost",
:port => 25, :port => 25,
@ -296,6 +296,9 @@ module ActionMailer #:nodoc:
@@default_implicit_parts_order = [ "text/html", "text/enriched", "text/plain" ] @@default_implicit_parts_order = [ "text/html", "text/enriched", "text/plain" ]
cattr_accessor :default_implicit_parts_order cattr_accessor :default_implicit_parts_order
cattr_reader :protected_instance_variables
@@protected_instance_variables = %w(@body)
# Specify the BCC addresses for the message # Specify the BCC addresses for the message
adv_attr_accessor :bcc adv_attr_accessor :bcc
@ -365,6 +368,7 @@ module ActionMailer #:nodoc:
# The mail object instance referenced by this mailer. # The mail object instance referenced by this mailer.
attr_reader :mail attr_reader :mail
attr_reader :template_name, :default_template_name, :action_name
class << self class << self
attr_writer :mailer_name attr_writer :mailer_name
@ -377,11 +381,16 @@ module ActionMailer #:nodoc:
alias_method :controller_name, :mailer_name alias_method :controller_name, :mailer_name
alias_method :controller_path, :mailer_name alias_method :controller_path, :mailer_name
def method_missing(method_symbol, *parameters)#:nodoc: def respond_to?(method_symbol, include_private = false) #:nodoc:
case method_symbol.id2name matches_dynamic_method?(method_symbol) || super
when /^create_([_a-z]\w*)/ then new($1, *parameters).mail end
when /^deliver_([_a-z]\w*)/ then new($1, *parameters).deliver!
when "new" then nil def method_missing(method_symbol, *parameters) #:nodoc:
match = matches_dynamic_method?(method_symbol)
case match[1]
when 'create' then new(match[2], *parameters).mail
when 'deliver' then new(match[2], *parameters).deliver!
when 'new' then nil
else super else super
end end
end end
@ -414,20 +423,24 @@ module ActionMailer #:nodoc:
new.deliver!(mail) new.deliver!(mail)
end end
# Register a template extension so mailer templates written in a
# templating language other than rhtml or rxml are supported.
# To use this, include in your template-language plugin's init
# code or on a per-application basis, this can be invoked from
# <tt>config/environment.rb</tt>:
#
# ActionMailer::Base.register_template_extension('haml')
def register_template_extension(extension) def register_template_extension(extension)
template_extensions << extension ActiveSupport::Deprecation.warn(
"ActionMailer::Base.register_template_extension has been deprecated." +
"Use ActionView::Base.register_template_extension instead", caller)
end
def template_root
self.view_paths && self.view_paths.first
end end
def template_root=(root) def template_root=(root)
write_inheritable_attribute(:template_root, root) self.view_paths = ActionView::Base.process_view_paths(root)
ActionView::TemplateFinder.process_view_paths(root) end
private
def matches_dynamic_method?(method_name) #:nodoc:
method_name = method_name.to_s
/(create|deliver)_([_a-z]\w*)/.match(method_name) || /^(new)$/.match(method_name)
end end
end end
@ -452,16 +465,18 @@ module ActionMailer #:nodoc:
# "the_template_file.text.html.erb", etc.). Only do this if parts # "the_template_file.text.html.erb", etc.). Only do this if parts
# have not already been specified manually. # have not already been specified manually.
if @parts.empty? if @parts.empty?
templates = Dir.glob("#{template_path}/#{@template}.*") Dir.glob("#{template_path}/#{@template}.*").each do |path|
templates.each do |path| template = template_root["#{mailer_name}/#{File.basename(path)}"]
basename = File.basename(path)
template_regex = Regexp.new("^([^\\\.]+)\\\.([^\\\.]+\\\.[^\\\.]+)\\\.(" + template_extensions.join('|') + ")$") # Skip unless template has a multipart format
next unless md = template_regex.match(basename) next unless template && template.multipart?
template_name = basename
content_type = md.captures[1].gsub('.', '/') @parts << Part.new(
@parts << Part.new(:content_type => content_type, :content_type => template.content_type,
:disposition => "inline", :charset => charset, :disposition => "inline",
:body => render_message(template_name, @body)) :charset => charset,
:body => render_message(template, @body)
)
end end
unless @parts.empty? unless @parts.empty?
@content_type = "multipart/alternative" @content_type = "multipart/alternative"
@ -474,7 +489,7 @@ module ActionMailer #:nodoc:
# normal template exists (or if there were no implicit parts) we render # normal template exists (or if there were no implicit parts) we render
# it. # it.
template_exists = @parts.empty? template_exists = @parts.empty?
template_exists ||= Dir.glob("#{template_path}/#{@template}.*").any? { |i| File.basename(i).split(".").length == 2 } template_exists ||= template_root["#{mailer_name}/#{@template}"]
@body = render_message(@template, @body) if template_exists @body = render_message(@template, @body) if template_exists
# Finally, if there are other message parts and a textual body exists, # Finally, if there are other message parts and a textual body exists,
@ -522,6 +537,7 @@ module ActionMailer #:nodoc:
@content_type ||= @@default_content_type.dup @content_type ||= @@default_content_type.dup
@implicit_parts_order ||= @@default_implicit_parts_order.dup @implicit_parts_order ||= @@default_implicit_parts_order.dup
@template ||= method_name @template ||= method_name
@default_template_name = @action_name = @template
@mailer_name ||= self.class.name.underscore @mailer_name ||= self.class.name.underscore
@parts ||= [] @parts ||= []
@headers ||= {} @headers ||= {}
@ -530,16 +546,38 @@ module ActionMailer #:nodoc:
end end
def render_message(method_name, body) def render_message(method_name, body)
render :file => method_name, :body => body, :use_full_path => true render :file => method_name, :body => body
end end
def render(opts) def render(opts)
body = opts.delete(:body) body = opts.delete(:body)
if opts[:file] && opts[:file] !~ /\// if opts[:file] && (opts[:file] !~ /\// && !opts[:file].respond_to?(:render))
opts[:file] = "#{mailer_name}/#{opts[:file]}" opts[:file] = "#{mailer_name}/#{opts[:file]}"
end end
opts[:use_full_path] = true
initialize_template_class(body).render(opts) begin
old_template, @template = @template, initialize_template_class(body)
layout = respond_to?(:pick_layout, true) ? pick_layout(opts) : false
@template.render(opts.merge(:layout => layout))
ensure
@template = old_template
end
end
def default_template_format
:html
end
def candidate_for_layout?(options)
!@template.send(:_exempt_from_layout?, default_template_name)
end
def template_root
self.class.template_root
end
def template_root=(root)
self.class.template_root = root
end end
def template_path def template_path
@ -547,7 +585,7 @@ module ActionMailer #:nodoc:
end end
def initialize_template_class(assigns) def initialize_template_class(assigns)
ActionView::Base.new([template_root], assigns, self) ActionView::Base.new(view_paths, assigns, self)
end end
def sort_parts(parts, order = []) def sort_parts(parts, order = [])

View file

@ -72,7 +72,7 @@ module ActionMailer
methods.flatten.each do |method| methods.flatten.each do |method|
master_helper_module.module_eval <<-end_eval master_helper_module.module_eval <<-end_eval
def #{method}(*args, &block) def #{method}(*args, &block)
controller.send!(%(#{method}), *args, &block) controller.__send__(%(#{method}), *args, &block)
end end
end_eval end_eval
end end
@ -92,7 +92,7 @@ module ActionMailer
inherited_without_helper(child) inherited_without_helper(child)
begin begin
child.master_helper_module = Module.new child.master_helper_module = Module.new
child.master_helper_module.send! :include, master_helper_module child.master_helper_module.__send__(:include, master_helper_module)
child.helper child.name.to_s.underscore child.helper child.name.to_s.underscore
rescue MissingSourceFile => e rescue MissingSourceFile => e
raise unless e.is_missing?("helpers/#{child.name.to_s.underscore}_helper") raise unless e.is_missing?("helpers/#{child.name.to_s.underscore}_helper")

View file

@ -38,7 +38,7 @@ module TMail
# = Class Address # = Class Address
# #
# Provides a complete handling library for email addresses. Can parse a string of an # Provides a complete handling library for email addresses. Can parse a string of an
# address directly or take in preformatted addresses themseleves. Allows you to add # address directly or take in preformatted addresses themselves. Allows you to add
# and remove phrases from the front of the address and provides a compare function for # and remove phrases from the front of the address and provides a compare function for
# email addresses. # email addresses.
# #
@ -143,7 +143,7 @@ module TMail
# This is to catch an unquoted "@" symbol in the local part of the # This is to catch an unquoted "@" symbol in the local part of the
# address. Handles addresses like <"@"@me.com> and makes sure they # address. Handles addresses like <"@"@me.com> and makes sure they
# stay like <"@"@me.com> (previously were becomming <@@me.com>) # stay like <"@"@me.com> (previously were becoming <@@me.com>)
if local && (local.join == '@' || local.join =~ /\A[^"].*?@.*?[^"]\Z/) if local && (local.join == '@' || local.join =~ /\A[^"].*?@.*?[^"]\Z/)
@local = "\"#{local.join}\"" @local = "\"#{local.join}\""
else else

View file

@ -59,7 +59,7 @@ module TMail
# #
# This is because a mailbox doesn't have the : after the From that designates the # This is because a mailbox doesn't have the : after the From that designates the
# beginning of the envelope sender (which can be different to the from address of # beginning of the envelope sender (which can be different to the from address of
# the emial) # the email)
# #
# Other fields can be passed as normal, "Reply-To", "Received" etc. # Other fields can be passed as normal, "Reply-To", "Received" etc.
# #

View file

@ -42,7 +42,7 @@ module TMail
# Allows you to query the mail object with a string to get the contents # Allows you to query the mail object with a string to get the contents
# of the field you want. # of the field you want.
# #
# Returns a string of the exact contnts of the field # Returns a string of the exact contents of the field
# #
# mail.from = "mikel <mikel@lindsaar.net>" # mail.from = "mikel <mikel@lindsaar.net>"
# mail.header_string("From") #=> "mikel <mikel@lindsaar.net>" # mail.header_string("From") #=> "mikel <mikel@lindsaar.net>"

View file

@ -255,7 +255,7 @@ module TMail
alias fetch [] alias fetch []
# Allows you to set or delete TMail header objects at will. # Allows you to set or delete TMail header objects at will.
# Eamples: # Examples:
# @mail = TMail::Mail.new # @mail = TMail::Mail.new
# @mail['to'].to_s # => 'mikel@test.com.au' # @mail['to'].to_s # => 'mikel@test.com.au'
# @mail['to'] = 'mikel@elsewhere.org' # @mail['to'] = 'mikel@elsewhere.org'
@ -265,7 +265,7 @@ module TMail
# @mail['to'].to_s # => nil # @mail['to'].to_s # => nil
# @mail.encoded # => "\r\n" # @mail.encoded # => "\r\n"
# #
# Note: setting mail[] = nil actualy deletes the header field in question from the object, # Note: setting mail[] = nil actually deletes the header field in question from the object,
# it does not just set the value of the hash to nil # it does not just set the value of the hash to nil
def []=( key, val ) def []=( key, val )
dkey = key.downcase dkey = key.downcase

View file

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

View file

@ -1,6 +1,8 @@
require 'test/unit' require 'test/unit'
$:.unshift "#{File.dirname(__FILE__)}/../lib" $:.unshift "#{File.dirname(__FILE__)}/../lib"
$:.unshift "#{File.dirname(__FILE__)}/../../activesupport/lib"
$:.unshift "#{File.dirname(__FILE__)}/../../actionpack/lib"
require 'action_mailer' require 'action_mailer'
require 'action_mailer/test_case' require 'action_mailer/test_case'

View file

@ -0,0 +1 @@
Inside

View file

@ -0,0 +1 @@
You logged out

View file

@ -0,0 +1 @@
We do not spam

View file

@ -0,0 +1 @@
Hello from layout <%= yield %>

View file

@ -0,0 +1 @@
Spammer layout <%= yield %>

View file

@ -0,0 +1,2 @@
body: <%= @body %>
bar: <%= @bar %>

View file

@ -0,0 +1,3 @@
Hello there,
Mr. <%= @recipient %>

View file

@ -0,0 +1,78 @@
require 'abstract_unit'
class AutoLayoutMailer < ActionMailer::Base
def hello(recipient)
recipients recipient
subject "You have a mail"
from "tester@example.com"
end
def spam(recipient)
recipients recipient
subject "You have a mail"
from "tester@example.com"
body render(:inline => "Hello, <%= @world %>", :layout => 'spam', :body => { :world => "Earth" })
end
def nolayout(recipient)
recipients recipient
subject "You have a mail"
from "tester@example.com"
body render(:inline => "Hello, <%= @world %>", :layout => false, :body => { :world => "Earth" })
end
end
class ExplicitLayoutMailer < ActionMailer::Base
layout 'spam', :except => [:logout]
def signup(recipient)
recipients recipient
subject "You have a mail"
from "tester@example.com"
end
def logout(recipient)
recipients recipient
subject "You have a mail"
from "tester@example.com"
end
end
class LayoutMailerTest < Test::Unit::TestCase
def setup
set_delivery_method :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
@recipient = 'test@localhost'
end
def teardown
restore_delivery_method
end
def test_should_pickup_default_layout
mail = AutoLayoutMailer.create_hello(@recipient)
assert_equal "Hello from layout Inside", mail.body.strip
end
def test_should_pickup_layout_given_to_render
mail = AutoLayoutMailer.create_spam(@recipient)
assert_equal "Spammer layout Hello, Earth", mail.body.strip
end
def test_should_respect_layout_false
mail = AutoLayoutMailer.create_nolayout(@recipient)
assert_equal "Hello, Earth", mail.body.strip
end
def test_explicit_class_layout
mail = ExplicitLayoutMailer.create_signup(@recipient)
assert_equal "Spammer layout We do not spam", mail.body.strip
end
def test_explicit_layout_exceptions
mail = ExplicitLayoutMailer.create_logout(@recipient)
assert_equal "You logged out", mail.body.strip
end
end

View file

@ -88,12 +88,6 @@ class RenderHelperTest < Test::Unit::TestCase
mail = RenderMailer.deliver_included_subtemplate(@recipient) mail = RenderMailer.deliver_included_subtemplate(@recipient)
assert_equal "Hey Ho, let's go!", mail.body.strip assert_equal "Hey Ho, let's go!", mail.body.strip
end end
def test_deprecated_old_subtemplate
assert_raises ActionView::ActionViewError do
RenderMailer.deliver_included_old_subtemplate(@recipient)
end
end
end end
class FirstSecondHelperTest < Test::Unit::TestCase class FirstSecondHelperTest < Test::Unit::TestCase

View file

@ -273,6 +273,13 @@ class TestMailer < ActionMailer::Base
headers "return-path" => "another@somewhere.test" headers "return-path" => "another@somewhere.test"
end end
def body_ivar(recipient)
recipients recipient
subject "Body as a local variable"
from "test@example.com"
body :body => "foo", :bar => "baz"
end
class <<self class <<self
attr_accessor :received_body attr_accessor :received_body
end end
@ -382,7 +389,6 @@ class ActionMailerTest < Test::Unit::TestCase
end end
def test_custom_templating_extension def test_custom_templating_extension
#
# N.b., custom_templating_extension.text.plain.haml is expected to be in fixtures/test_mailer directory # N.b., custom_templating_extension.text.plain.haml is expected to be in fixtures/test_mailer directory
expected = new_mail expected = new_mail
expected.to = @recipient expected.to = @recipient
@ -394,14 +400,6 @@ class ActionMailerTest < Test::Unit::TestCase
# Stub the render method so no alternative renderers need be present. # Stub the render method so no alternative renderers need be present.
ActionView::Base.any_instance.stubs(:render).returns("Hello there, \n\nMr. #{@recipient}") ActionView::Base.any_instance.stubs(:render).returns("Hello there, \n\nMr. #{@recipient}")
# If the template is not registered, there should be no parts.
created = nil
assert_nothing_raised { created = TestMailer.create_custom_templating_extension(@recipient) }
assert_not_nil created
assert_equal 0, created.parts.length
ActionMailer::Base.register_template_extension('haml')
# Now that the template is registered, there should be one part. The text/plain part. # Now that the template is registered, there should be one part. The text/plain part.
created = nil created = nil
assert_nothing_raised { created = TestMailer.create_custom_templating_extension(@recipient) } assert_nothing_raised { created = TestMailer.create_custom_templating_extension(@recipient) }
@ -935,6 +933,11 @@ EOF
TestMailer.deliver_return_path TestMailer.deliver_return_path
assert_match %r{^Return-Path: <another@somewhere.test>}, MockSMTP.deliveries[0][0] assert_match %r{^Return-Path: <another@somewhere.test>}, MockSMTP.deliveries[0][0]
end end
def test_body_is_stored_as_an_ivar
mail = TestMailer.create_body_ivar(@recipient)
assert_equal "body: foo\nbar: baz", mail.body
end
end end
end # uses_mocha end # uses_mocha
@ -977,3 +980,55 @@ class MethodNamingTest < Test::Unit::TestCase
end end
end end
end end
class RespondToTest < Test::Unit::TestCase
class RespondToMailer < ActionMailer::Base; end
def setup
set_delivery_method :test
end
def teardown
restore_delivery_method
end
def test_should_respond_to_new
assert RespondToMailer.respond_to?(:new)
end
def test_should_respond_to_create_with_template_suffix
assert RespondToMailer.respond_to?(:create_any_old_template)
end
def test_should_respond_to_deliver_with_template_suffix
assert RespondToMailer.respond_to?(:deliver_any_old_template)
end
def test_should_not_respond_to_new_with_template_suffix
assert !RespondToMailer.respond_to?(:new_any_old_template)
end
def test_should_not_respond_to_create_with_template_suffix_unless_it_is_separated_by_an_underscore
assert !RespondToMailer.respond_to?(:createany_old_template)
end
def test_should_not_respond_to_deliver_with_template_suffix_unless_it_is_separated_by_an_underscore
assert !RespondToMailer.respond_to?(:deliverany_old_template)
end
def test_should_not_respond_to_create_with_template_suffix_if_it_begins_with_a_uppercase_letter
assert !RespondToMailer.respond_to?(:create_Any_old_template)
end
def test_should_not_respond_to_deliver_with_template_suffix_if_it_begins_with_a_uppercase_letter
assert !RespondToMailer.respond_to?(:deliver_Any_old_template)
end
def test_should_not_respond_to_create_with_template_suffix_if_it_begins_with_a_digit
assert !RespondToMailer.respond_to?(:create_1_template)
end
def test_should_not_respond_to_deliver_with_template_suffix_if_it_begins_with_a_digit
assert !RespondToMailer.respond_to?(:deliver_1_template)
end
end

View file

@ -1,19 +1,206 @@
*2.1.1 (September 4th, 2008)* *2.2.0 [RC1] (October 24th, 2008)*
* Fix incorrect closing CDATA delimiter and that HTML::Node.parse would blow up on unclosed CDATA sections [packagethief]
* Added stale? and fresh_when methods to provide a layer of abstraction above request.fresh? and friends [DHH]. Example:
class ArticlesController < ApplicationController
def show_with_respond_to_block
@article = Article.find(params[:id])
# If the request sends headers that differs from the options provided to stale?, then
# the request is indeed stale and the respond_to block is triggered (and the options
# to the stale? call is set on the response).
#
# If the request headers match, then the request is fresh and the respond_to block is
# not triggered. Instead the default render will occur, which will check the last-modified
# and etag headers and conclude that it only needs to send a "304 Not Modified" instead
# of rendering the template.
if stale?(:last_modified => @article.published_at.utc, :etag => @article)
respond_to do |wants|
# normal response processing
end
end
end
def show_with_implied_render
@article = Article.find(params[:id])
# Sets the response headers and checks them against the request, if the request is stale
# (i.e. no match of either etag or last-modified), then the default render of the template happens.
# If the request is fresh, then the default render will return a "304 Not Modified"
# instead of rendering the template.
fresh_when(:last_modified => @article.published_at.utc, :etag => @article)
end
end
* Added inline builder yield to atom_feed_helper tags where appropriate [Sam Ruby]. Example:
entry.summary :type => 'xhtml' do |xhtml|
xhtml.p pluralize(order.line_items.count, "line item")
xhtml.p "Shipped to #{order.address}"
xhtml.p "Paid by #{order.pay_type}"
end
* Make PrototypeHelper#submit_to_remote a wrapper around PrototypeHelper#button_to_remote. [Tarmo Tänav]
* Set HttpOnly for the cookie session store's cookie. #1046
* Added FormTagHelper#image_submit_tag confirm option #784 [Alastair Brunton]
* Fixed FormTagHelper#submit_tag with :disable_with option wouldn't submit the button's value when was clicked #633 [Jose Fernandez]
* Stopped logging template compiles as it only clogs up the log [DHH]
* Changed the X-Runtime header to report in milliseconds [DHH]
* Changed BenchmarkHelper#benchmark to report in milliseconds [DHH]
* Changed logging format to be millisecond based and skip misleading stats [DHH]. Went from:
Completed in 0.10000 (4 reqs/sec) | Rendering: 0.04000 (40%) | DB: 0.00400 (4%) | 200 OK [http://example.com]
...to:
Completed in 100ms (View: 40, DB: 4) | 200 OK [http://example.com]
* Add support for shallow nesting of routes. #838 [S. Brent Faulkner]
Example :
map.resources :users, :shallow => true do |user|
user.resources :posts
end
- GET /users/1/posts (maps to PostsController#index action as usual)
named route "user_posts" is added as usual.
- GET /posts/2 (maps to PostsController#show action as if it were not nested)
Additionally, named route "post" is added too.
* Added button_to_remote helper. #3641 [Donald Piret, Tarmo Tänav]
* Deprecate render_component. Please use render_component plugin from http://github.com/rails/render_component/tree/master [Pratik]
* Routes may be restricted to lists of HTTP methods instead of a single method or :any. #407 [Brennan Dunn, Gaius Centus Novus]
map.resource :posts, :collection => { :search => [:get, :post] }
map.session 'session', :requirements => { :method => [:get, :post, :delete] }
* Deprecated implicit local assignments when rendering partials [Josh Peek]
* Introduce current_cycle helper method to return the current value without bumping the cycle. #417 [Ken Collins]
* Allow polymorphic_url helper to take url options. #880 [Tarmo Tänav]
* Switched integration test runner to use Rack processor instead of CGI [Josh Peek]
* Made AbstractRequest.if_modified_sense return nil if the header could not be parsed [Jamis Buck]
* Added back ActionController::Base.allow_concurrency flag [Josh Peek]
* AbstractRequest.relative_url_root is no longer automatically configured by a HTTP header. It can now be set in your configuration environment with config.action_controller.relative_url_root [Josh Peek]
* Update Prototype to 1.6.0.2 #599 [Patrick Joyce]
* Conditional GET utility methods. [Jeremy Kemper]
response.last_modified = @post.updated_at
response.etag = [:admin, @post, current_user]
if request.fresh?(response)
head :not_modified
else
# render ...
end
* All 2xx requests are considered successful [Josh Peek] * All 2xx requests are considered successful [Josh Peek]
* Deprecate the limited follow_redirect in functional tests. If you wish to follow redirects, use integration tests. [Michael Koziarski]
* Fixed that AssetTagHelper#compute_public_path shouldn't cache the asset_host along with the source or per-request proc's won't run [DHH] * Fixed that AssetTagHelper#compute_public_path shouldn't cache the asset_host along with the source or per-request proc's won't run [DHH]
* Deprecate define_javascript_functions, javascript_include_tag and friends are much better [Michael Koziarski] * Removed config.action_view.cache_template_loading, use config.cache_classes instead [Josh Peek]
* Get buffer for fragment cache from template's @output_buffer [Josh Peek]
* Set config.action_view.warn_cache_misses = true to receive a warning if you perform an action that results in an expensive disk operation that could be cached [Josh Peek]
* Refactor template preloading. New abstractions include Renderable mixins and a refactored Template class [Josh Peek]
* Changed ActionView::TemplateHandler#render API method signature to render(template, local_assigns = {}) [Josh Peek]
* Changed PrototypeHelper#submit_to_remote to PrototypeHelper#button_to_remote to stay consistent with link_to_remote (submit_to_remote still works as an alias) #8994 [clemens]
* Add :recursive option to javascript_include_tag and stylesheet_link_tag to be used along with :all. #480 [Damian Janowski]
* Allow users to disable the use of the Accept header [Michael Koziarski]
The accept header is poorly implemented by browsers and causes strange
errors when used on public sites where crawlers make requests too. You
can use formatted urls (e.g. /people/1.xml) to support API clients in a
much simpler way.
To disable the header you need to set:
config.action_controller.use_accept_header = false
* Do not stat template files in production mode before rendering. You will no longer be able to modify templates in production mode without restarting the server [Josh Peek]
* Deprecated TemplateHandler line offset [Josh Peek]
* Allow caches_action to accept cache store options. #416. [José Valim]. Example:
caches_action :index, :redirected, :if => Proc.new { |c| !c.request.format.json? }, :expires_in => 1.hour
* Remove define_javascript_functions, javascript_include_tag and friends are far superior. [Michael Koziarski]
* Deprecate :use_full_path render option. The supplying the option no longer has an effect [Josh Peek]
* Add :as option to render a collection of partials with a custom local variable name. #509 [Simon Jefford, Pratik Naik]
render :partial => 'other_people', :collection => @people, :as => :person
This will let you access objects of @people as 'person' local variable inside 'other_people' partial template.
* time_zone_select: support for regexp matching of priority zones. Resolves #195 [Ernie Miller]
* Made ActionView::Base#render_file private [Josh Peek]
* Refactor and simplify the implementation of assert_redirected_to. Arguments are now normalised relative to the controller being tested, not the root of the application. [Michael Koziarski]
This could cause some erroneous test failures if you were redirecting between controllers
in different namespaces and wrote your assertions relative to the root of the application.
* Remove follow_redirect from controller functional tests.
If you want to follow redirects you can use integration tests. The functional test
version was only useful if you were using redirect_to :id=>...
* Fix polymorphic_url with singleton resources. #461 [Tammer Saleh] * Fix polymorphic_url with singleton resources. #461 [Tammer Saleh]
* Deprecate ActionView::Base.erb_variable. Use the concat helper method instead of appending to it directly. [Jeremy Kemper] * Replaced TemplateFinder abstraction with ViewLoadPaths [Josh Peek]
* Added block-call style to link_to [Sam Stephenson/DHH]. Example:
<% link_to(@profile) do %>
<strong><%= @profile.name %></strong> -- <span>Check it out!!</span>
<% end %>
* Performance: integration test benchmarking and profiling. [Jeremy Kemper]
* Make caching more aware of mime types. Ensure request format is not considered while expiring cache. [Jonathan del Strother]
* Drop ActionController::Base.allow_concurrency flag [Josh Peek]
* More efficient concat and capture helpers. Remove ActionView::Base.erb_variable. [Jeremy Kemper]
* Added page.reload functionality. Resolves #277. [Sean Huber]
* Fixed Request#remote_ip to only raise hell if the HTTP_CLIENT_IP and HTTP_X_FORWARDED_FOR doesn't match (not just if they're both present) [Mark Imbriaco, Bradford Folkens] * Fixed Request#remote_ip to only raise hell if the HTTP_CLIENT_IP and HTTP_X_FORWARDED_FOR doesn't match (not just if they're both present) [Mark Imbriaco, Bradford Folkens]
* Allow caches_action to accept a layout option [José Valim]
* Added Rack processor [Ezra Zygmuntowicz, Josh Peek]
*2.1.0 (May 31st, 2008)* *2.1.0 (May 31st, 2008)*

View file

@ -31,7 +31,7 @@ http://www.rubyonrails.org.
A short rundown of the major features: A short rundown of the major features:
* Actions grouped in controller as methods instead of separate command objects * Actions grouped in controller as methods instead of separate command objects
and can therefore share helper methods. and can therefore share helper methods
BlogController < ActionController::Base BlogController < ActionController::Base
def show def show
@ -168,7 +168,7 @@ A short rundown of the major features:
{Learn more}[link:classes/ActionController/Base.html] {Learn more}[link:classes/ActionController/Base.html]
* Javascript and Ajax integration. * Javascript and Ajax integration
link_to_function "Greeting", "alert('Hello world!')" link_to_function "Greeting", "alert('Hello world!')"
link_to_remote "Delete this post", :update => "posts", link_to_remote "Delete this post", :update => "posts",
@ -177,7 +177,7 @@ A short rundown of the major features:
{Learn more}[link:classes/ActionView/Helpers/JavaScriptHelper.html] {Learn more}[link:classes/ActionView/Helpers/JavaScriptHelper.html]
* Pagination for navigating lists of results. * Pagination for navigating lists of results
# controller # controller
def list def list
@ -192,15 +192,9 @@ A short rundown of the major features:
{Learn more}[link:classes/ActionController/Pagination.html] {Learn more}[link:classes/ActionController/Pagination.html]
* Easy testing of both controller and template result through TestRequest/Response * Easy testing of both controller and rendered template through ActionController::TestCase
class LoginControllerTest < Test::Unit::TestCase
def setup
@controller = LoginController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
class LoginControllerTest < ActionController::TestCase
def test_failing_authenticate def test_failing_authenticate
process :authenticate, :user_name => "nop", :password => "" process :authenticate, :user_name => "nop", :password => ""
assert flash.has_key?(:alert) assert flash.has_key?(:alert)
@ -208,7 +202,7 @@ A short rundown of the major features:
end end
end end
{Learn more}[link:classes/ActionController/TestRequest.html] {Learn more}[link:classes/ActionController/TestCase.html]
* Automated benchmarking and integrated logging * Automated benchmarking and integrated logging

View file

@ -5,8 +5,6 @@ require 'rake/rdoctask'
require 'rake/packagetask' require 'rake/packagetask'
require 'rake/gempackagetask' require 'rake/gempackagetask'
require 'rake/contrib/sshpublisher' require 'rake/contrib/sshpublisher'
require 'rake/contrib/rubyforgepublisher'
require File.join(File.dirname(__FILE__), 'lib', 'action_pack', 'version') require File.join(File.dirname(__FILE__), 'lib', 'action_pack', 'version')
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
@ -27,14 +25,16 @@ task :default => [ :test ]
desc "Run all unit tests" desc "Run all unit tests"
task :test => [:test_action_pack, :test_active_record_integration] task :test => [:test_action_pack, :test_active_record_integration]
Rake::TestTask.new(:test_action_pack) { |t| Rake::TestTask.new(:test_action_pack) do |t|
t.libs << "test" t.libs << "test"
# make sure we include the tests in alphabetical order as on some systems
# this will not happen automatically and the tests (as a whole) will error # make sure we include the tests in alphabetical order as on some systems
t.test_files=Dir.glob( "test/[cft]*/**/*_test.rb" ).sort # this will not happen automatically and the tests (as a whole) will error
# t.pattern = 'test/*/*_test.rb' t.test_files = Dir.glob( "test/[cft]*/**/*_test.rb" ).sort
t.verbose = true t.verbose = true
} #t.warning = true
end
desc 'ActiveRecord Integration Tests' desc 'ActiveRecord Integration Tests'
Rake::TestTask.new(:test_active_record_integration) do |t| Rake::TestTask.new(:test_active_record_integration) do |t|
@ -80,7 +80,7 @@ spec = Gem::Specification.new do |s|
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'
s.add_dependency('activesupport', '= 2.1.1' + PKG_BUILD) s.add_dependency('activesupport', '= 2.2.0' + PKG_BUILD)
s.require_path = 'lib' s.require_path = 'lib'
s.autorequire = 'action_controller' s.autorequire = 'action_controller'
@ -136,8 +136,8 @@ task :update_js => [ :update_scriptaculous ]
desc "Publish the API documentation" desc "Publish the API documentation"
task :pgem => [:package] do task :pgem => [:package] do
Rake::SshFilePublisher.new("david@greed.loudthinking.com", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
`ssh david@greed.loudthinking.com '/u/sites/gems/gemupdate.sh'` `ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'`
end end
desc "Publish the API documentation" desc "Publish the API documentation"

View file

@ -21,16 +21,13 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++ #++
$:.unshift(File.dirname(__FILE__)) unless begin
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) require 'active_support'
rescue LoadError
unless defined?(ActiveSupport) activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
begin if File.directory?(activesupport_path)
$:.unshift "#{File.dirname(__FILE__)}/../../activesupport/lib" $:.unshift activesupport_path
require 'active_support' require 'active_support'
rescue LoadError
require 'rubygems'
gem 'activesupport'
end end
end end
@ -53,9 +50,11 @@ require 'action_controller/streaming'
require 'action_controller/session_management' require 'action_controller/session_management'
require 'action_controller/http_authentication' require 'action_controller/http_authentication'
require 'action_controller/components' require 'action_controller/components'
require 'action_controller/rack_process'
require 'action_controller/record_identifier' require 'action_controller/record_identifier'
require 'action_controller/request_forgery_protection' require 'action_controller/request_forgery_protection'
require 'action_controller/headers' require 'action_controller/headers'
require 'action_controller/translation'
require 'action_view' require 'action_view'
@ -76,4 +75,5 @@ ActionController::Base.class_eval do
include ActionController::Components include ActionController::Components
include ActionController::RecordIdentifier include ActionController::RecordIdentifier
include ActionController::RequestForgeryProtection include ActionController::RequestForgeryProtection
include ActionController::Translation
end end

View file

@ -56,74 +56,24 @@ module ActionController
# # assert that the redirection was to the named route login_url # # assert that the redirection was to the named route login_url
# assert_redirected_to login_url # assert_redirected_to login_url
# #
# # assert that the redirection was to the url for @customer
# assert_redirected_to @customer
#
def assert_redirected_to(options = {}, message=nil) def assert_redirected_to(options = {}, message=nil)
clean_backtrace do clean_backtrace do
assert_response(:redirect, message) assert_response(:redirect, message)
return true if options == @response.redirected_to return true if options == @response.redirected_to
ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
begin # Support partial arguments for hash redirections
url = {} if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash)
original = { :expected => options, :actual => @response.redirected_to.is_a?(Symbol) ? @response.redirected_to : @response.redirected_to.dup } return true if options.all? {|(key, value)| @response.redirected_to[key] == value}
original.each do |key, value|
if value.is_a?(Symbol)
value = @controller.respond_to?(value, true) ? @controller.send(value) : @controller.send("hash_for_#{value}_url")
end end
unless value.is_a?(Hash) redirected_to_after_normalisation = normalize_argument_to_redirection(@response.redirected_to)
request = case value options_after_normalisation = normalize_argument_to_redirection(options)
when NilClass then nil
when /^\w+:\/\// then recognized_request_for(%r{^(\w+://.*?(/|$|\?))(.*)$} =~ value ? $3 : nil)
else recognized_request_for(value)
end
value = request.path_parameters if request
end
if value.is_a?(Hash) # stringify 2 levels of hash keys if redirected_to_after_normalisation != options_after_normalisation
if name = value.delete(:use_route) flunk "Expected response to be a redirect to <#{options_after_normalisation}> but was a redirect to <#{redirected_to_after_normalisation}>"
route = ActionController::Routing::Routes.named_routes[name]
value.update(route.parameter_shell)
end
value.stringify_keys!
value.values.select { |v| v.is_a?(Hash) }.collect { |v| v.stringify_keys! }
if key == :expected && value['controller'] == @controller.controller_name && original[:actual].is_a?(Hash)
original[:actual].stringify_keys!
value.delete('controller') if original[:actual]['controller'].nil? || original[:actual]['controller'] == value['controller']
end
end
if value.respond_to?(:[]) && value['controller']
value['controller'] = value['controller'].to_s
if key == :actual && value['controller'].first != '/' && !value['controller'].include?('/')
new_controller_path = ActionController::Routing.controller_relative_to(value['controller'], @controller.class.controller_path)
value['controller'] = new_controller_path if value['controller'] != new_controller_path && ActionController::Routing.possible_controllers.include?(new_controller_path) && @response.redirected_to.is_a?(Hash)
end
value['controller'] = value['controller'][1..-1] if value['controller'].first == '/' # strip leading hash
end
url[key] = value
end
@response_diff = url[:actual].diff(url[:expected]) if url[:actual]
msg = build_message(message, "expected a redirect to <?>, found one to <?>, a difference of <?> ", url[:expected], url[:actual], @response_diff)
assert_block(msg) do
url[:expected].keys.all? do |k|
if k == :controller then url[:expected][k] == ActionController::Routing.controller_relative_to(url[:actual][k], @controller.class.controller_path)
else parameterize(url[:expected][k]) == parameterize(url[:actual][k])
end
end
end
rescue ActionController::RoutingError # routing failed us, so match the strings only.
msg = build_message(message, "expected a redirect to <?>, found one to <?>", options, @response.redirect_url)
url_regexp = %r{^(\w+://.*?(/|$|\?))(.*)$}
eurl, epath, url, path = [options, @response.redirect_url].collect do |url|
u, p = (url_regexp =~ url) ? [$1, $3] : [nil, url]
[u, (p.first == '/') ? p : '/' + p]
end.flatten
assert_equal(eurl, url, msg) if eurl && url
assert_equal(epath, path, msg) if epath && path
end end
end end
end end
@ -137,36 +87,37 @@ module ActionController
# #
def assert_template(expected = nil, message=nil) def assert_template(expected = nil, message=nil)
clean_backtrace do clean_backtrace do
rendered = expected ? @response.rendered_file(!expected.include?('/')) : @response.rendered_file rendered = @response.rendered_template.to_s
msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered) msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered)
assert_block(msg) do assert_block(msg) do
if expected.nil? if expected.nil?
!@response.rendered_with_file? @response.rendered_template.blank?
else else
expected == rendered rendered.to_s.match(expected)
end end
end end
end end
end end
private private
# Recognizes the route for a given path.
def recognized_request_for(path, request_method = nil)
path = "/#{path}" unless path.first == '/'
# Assume given controller
request = ActionController::TestRequest.new({}, {}, nil)
request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method
request.path = path
ActionController::Routing::Routes.recognize(request)
request
end
# Proxy to to_param if the object will respond to it. # Proxy to to_param if the object will respond to it.
def parameterize(value) def parameterize(value)
value.respond_to?(:to_param) ? value.to_param : value value.respond_to?(:to_param) ? value.to_param : value
end end
def normalize_argument_to_redirection(fragment)
after_routing = @controller.url_for(fragment)
if after_routing =~ %r{^\w+://.*}
after_routing
else
# FIXME - this should probably get removed.
if after_routing.first != '/'
after_routing = '/' + after_routing
end
@request.protocol + @request.host_with_port + after_routing
end
end
end end
end end
end end

View file

@ -10,32 +10,32 @@ module ActionController
# and a :method containing the required HTTP verb. # and a :method containing the required HTTP verb.
# #
# # assert that POSTing to /items will call the create action on ItemsController # # assert that POSTing to /items will call the create action on ItemsController
# assert_recognizes({:controller => 'items', :action => 'create'}, {:path => 'items', :method => :post}) # assert_recognizes {:controller => 'items', :action => 'create'}, {:path => 'items', :method => :post}
# #
# You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used # You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used
# to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the # to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the
# extras argument, appending the query string on the path directly will not work. For example: # extras argument, appending the query string on the path directly will not work. For example:
# #
# # assert that a path of '/items/list/1?view=print' returns the correct options # # assert that a path of '/items/list/1?view=print' returns the correct options
# assert_recognizes({:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" }) # assert_recognizes {:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" }
# #
# The +message+ parameter allows you to pass in an error message that is displayed upon failure. # The +message+ parameter allows you to pass in an error message that is displayed upon failure.
# #
# ==== Examples # ==== Examples
# # Check the default route (i.e., the index action) # # Check the default route (i.e., the index action)
# assert_recognizes({:controller => 'items', :action => 'index'}, 'items') # assert_recognizes {:controller => 'items', :action => 'index'}, 'items'
# #
# # Test a specific action # # Test a specific action
# assert_recognizes({:controller => 'items', :action => 'list'}, 'items/list') # assert_recognizes {:controller => 'items', :action => 'list'}, 'items/list'
# #
# # Test an action with a parameter # # Test an action with a parameter
# assert_recognizes({:controller => 'items', :action => 'destroy', :id => '1'}, 'items/destroy/1') # assert_recognizes {:controller => 'items', :action => 'destroy', :id => '1'}, 'items/destroy/1'
# #
# # Test a custom route # # Test a custom route
# assert_recognizes({:controller => 'items', :action => 'show', :id => '1'}, 'view/item1') # assert_recognizes {:controller => 'items', :action => 'show', :id => '1'}, 'view/item1'
# #
# # Check a Simply RESTful generated route # # Check a Simply RESTful generated route
# assert_recognizes(list_items_url, 'items/list') # assert_recognizes list_items_url, 'items/list'
def assert_recognizes(expected_options, path, extras={}, message=nil) def assert_recognizes(expected_options, path, extras={}, message=nil)
if path.is_a? Hash if path.is_a? Hash
request_method = path[:method] request_method = path[:method]
@ -67,13 +67,13 @@ module ActionController
# #
# ==== Examples # ==== Examples
# # Asserts that the default action is generated for a route with no action # # Asserts that the default action is generated for a route with no action
# assert_generates("/items", :controller => "items", :action => "index") # assert_generates "/items", :controller => "items", :action => "index"
# #
# # Tests that the list action is properly routed # # Tests that the list action is properly routed
# assert_generates("/items/list", :controller => "items", :action => "list") # assert_generates "/items/list", :controller => "items", :action => "list"
# #
# # Tests the generation of a route with a parameter # # Tests the generation of a route with a parameter
# assert_generates("/items/list/1", { :controller => "items", :action => "list", :id => "1" }) # assert_generates "/items/list/1", { :controller => "items", :action => "list", :id => "1" }
# #
# # Asserts that the generated route gives us our custom route # # Asserts that the generated route gives us our custom route
# assert_generates "changesets/12", { :controller => 'scm', :action => 'show_diff', :revision => "12" } # assert_generates "changesets/12", { :controller => 'scm', :action => 'show_diff', :revision => "12" }
@ -104,19 +104,19 @@ module ActionController
# #
# ==== Examples # ==== Examples
# # Assert a basic route: a controller with the default action (index) # # Assert a basic route: a controller with the default action (index)
# assert_routing('/home', :controller => 'home', :action => 'index') # assert_routing '/home', :controller => 'home', :action => 'index'
# #
# # Test a route generated with a specific controller, action, and parameter (id) # # Test a route generated with a specific controller, action, and parameter (id)
# assert_routing('/entries/show/23', :controller => 'entries', :action => 'show', id => 23) # assert_routing '/entries/show/23', :controller => 'entries', :action => 'show', id => 23
# #
# # Assert a basic route (controller + default action), with an error message if it fails # # Assert a basic route (controller + default action), with an error message if it fails
# assert_routing('/store', { :controller => 'store', :action => 'index' }, {}, {}, 'Route for store index not generated properly') # assert_routing '/store', { :controller => 'store', :action => 'index' }, {}, {}, 'Route for store index not generated properly'
# #
# # Tests a route, providing a defaults hash # # Tests a route, providing a defaults hash
# assert_routing 'controller/action/9', {:id => "9", :item => "square"}, {:controller => "controller", :action => "action"}, {}, {:item => "square"} # assert_routing 'controller/action/9', {:id => "9", :item => "square"}, {:controller => "controller", :action => "action"}, {}, {:item => "square"}
# #
# # Tests a route with a HTTP method # # Tests a route with a HTTP method
# assert_routing({ :method => 'put', :path => '/product/321' }, { :controller => "product", :action => "update", :id => "321" }) # assert_routing { :method => 'put', :path => '/product/321' }, { :controller => "product", :action => "update", :id => "321" }
def assert_routing(path, options, defaults={}, extras={}, message=nil) def assert_routing(path, options, defaults={}, extras={}, message=nil)
assert_recognizes(options, path, extras, message) assert_recognizes(options, path, extras, message)

View file

@ -21,10 +21,8 @@ module ActionController
# from the response HTML or elements selected by the enclosing assertion. # from the response HTML or elements selected by the enclosing assertion.
# #
# In addition to HTML responses, you can make the following assertions: # In addition to HTML responses, you can make the following assertions:
# * +assert_select_rjs+ - Assertions on HTML content of RJS update and # * +assert_select_rjs+ - Assertions on HTML content of RJS update and insertion operations.
# insertion operations. # * +assert_select_encoded+ - Assertions on HTML encoded inside XML, for example for dealing with feed item descriptions.
# * +assert_select_encoded+ - Assertions on HTML encoded inside XML,
# for example for dealing with feed item descriptions.
# * +assert_select_email+ - Assertions on the HTML body of an e-mail. # * +assert_select_email+ - Assertions on the HTML body of an e-mail.
# #
# Also see HTML::Selector to learn how to use selectors. # Also see HTML::Selector to learn how to use selectors.
@ -451,7 +449,13 @@ module ActionController
matches matches
else else
# RJS statement not found. # RJS statement not found.
flunk args.shift || "No RJS statement that replaces or inserts HTML content." case rjs_type
when :remove, :show, :hide, :toggle
flunk_message = "No RJS statement that #{rjs_type.to_s}s '#{id}' was rendered."
else
flunk_message = "No RJS statement that replaces or inserts HTML content."
end
flunk args.shift || flunk_message
end end
end end

View file

@ -252,7 +252,7 @@ module ActionController #:nodoc:
# #
# def do_something # def do_something
# redirect_to(:action => "elsewhere") and return if monkeys.nil? # redirect_to(:action => "elsewhere") and return if monkeys.nil?
# render :action => "overthere" # won't be called unless monkeys is nil # render :action => "overthere" # won't be called if monkeys is nil
# end # end
# #
class Base class Base
@ -260,8 +260,9 @@ module ActionController #:nodoc:
include StatusCodes include StatusCodes
cattr_reader :protected_instance_variables
# Controller specific instance variables which will not be accessible inside views. # Controller specific instance variables which will not be accessible inside views.
@@protected_view_variables = %w(@assigns @performed_redirect @performed_render @variables_added @request_origin @url @parent_controller @@protected_instance_variables = %w(@assigns @performed_redirect @performed_render @variables_added @request_origin @url @parent_controller
@action_name @before_filter_chain_aborted @action_cache_path @_session @_cookies @_headers @_params @action_name @before_filter_chain_aborted @action_cache_path @_session @_cookies @_headers @_params
@_flash @_response) @_flash @_response)
@ -283,10 +284,9 @@ module ActionController #:nodoc:
@@debug_routes = true @@debug_routes = true
cattr_accessor :debug_routes cattr_accessor :debug_routes
# Indicates to Mongrel or Webrick whether to allow concurrent action # Indicates whether to allow concurrent action processing. Your
# processing. Your controller actions and any other code they call must # controller actions and any other code they call must also behave well
# also behave well when called from concurrent threads. Turned off by # when called from concurrent threads. Turned off by default.
# default.
@@allow_concurrency = false @@allow_concurrency = false
cattr_accessor :allow_concurrency cattr_accessor :allow_concurrency
@ -347,10 +347,29 @@ module ActionController #:nodoc:
cattr_accessor :optimise_named_routes cattr_accessor :optimise_named_routes
self.optimise_named_routes = true self.optimise_named_routes = true
# Indicates whether the response format should be determined by examining the Accept HTTP header,
# or by using the simpler params + ajax rules.
#
# If this is set to +true+ (the default) then +respond_to+ and +Request#format+ will take the Accept
# header into account. If it is set to false then the request format will be determined solely
# by examining params[:format]. If params format is missing, the format will be either HTML or
# Javascript depending on whether the request is an AJAX request.
cattr_accessor :use_accept_header
self.use_accept_header = true
# Controls whether request forgergy protection is turned on or not. Turned off by default only in test mode. # Controls whether request forgergy protection is turned on or not. Turned off by default only in test mode.
class_inheritable_accessor :allow_forgery_protection class_inheritable_accessor :allow_forgery_protection
self.allow_forgery_protection = true self.allow_forgery_protection = true
# If you are deploying to a subdirectory, you will need to set
# <tt>config.action_controller.relative_url_root</tt>
# This defaults to ENV['RAILS_RELATIVE_URL_ROOT']
cattr_writer :relative_url_root
def self.relative_url_root
@@relative_url_root || ENV['RAILS_RELATIVE_URL_ROOT']
end
# Holds the request object that's primarily used to get environment variables through access like # Holds the request object that's primarily used to get environment variables through access like
# <tt>request.env["REQUEST_URI"]</tt>. # <tt>request.env["REQUEST_URI"]</tt>.
attr_internal :request attr_internal :request
@ -373,16 +392,9 @@ module ActionController #:nodoc:
# directive. Values should always be specified as strings. # directive. Values should always be specified as strings.
attr_internal :headers attr_internal :headers
# 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
# Returns the name of the action this controller is processing. # Returns the name of the action this controller is processing.
attr_accessor :action_name attr_accessor :action_name
# Templates that are exempt from layouts
@@exempt_from_layout = Set.new([/\.rjs$/])
class << self class << self
# Factory for the standard create, process loop where the controller is discarded after processing. # Factory for the standard create, process loop where the controller is discarded after processing.
def process(request, response) #:nodoc: def process(request, response) #:nodoc:
@ -408,28 +420,27 @@ module ActionController #:nodoc:
# By default, all methods defined in ActionController::Base and included modules are hidden. # By default, all methods defined in ActionController::Base and included modules are hidden.
# More methods can be hidden using <tt>hide_actions</tt>. # More methods can be hidden using <tt>hide_actions</tt>.
def hidden_actions def hidden_actions
unless read_inheritable_attribute(:hidden_actions) read_inheritable_attribute(:hidden_actions) || write_inheritable_attribute(:hidden_actions, [])
write_inheritable_attribute(:hidden_actions, ActionController::Base.public_instance_methods.map(&:to_s))
end
read_inheritable_attribute(:hidden_actions)
end end
# Hide each of the given methods from being callable as actions. # Hide each of the given methods from being callable as actions.
def hide_action(*names) def hide_action(*names)
write_inheritable_attribute(:hidden_actions, hidden_actions | names.map(&:to_s)) write_inheritable_attribute(:hidden_actions, hidden_actions | names.map { |name| name.to_s })
end end
## View load paths determine the bases from which template references can be made. So a call to # View load paths determine the bases from which template references can be made. So a call to
## render("test/template") will be looked up in the view load paths array and the closest match will be # render("test/template") will be looked up in the view load paths array and the closest match will be
## returned. # returned.
def view_paths def view_paths
@view_paths || superclass.view_paths if defined? @view_paths
@view_paths
else
superclass.view_paths
end
end end
def view_paths=(value) def view_paths=(value)
@view_paths = value @view_paths = ActionView::Base.process_view_paths(value) if value
ActionView::TemplateFinder.process_view_paths(value)
end end
# Adds a view_path to the front of the view_paths array. # Adds a view_path to the front of the view_paths array.
@ -440,9 +451,8 @@ module ActionController #:nodoc:
# ArticleController.prepend_view_path(["views/default", "views/custom"]) # ArticleController.prepend_view_path(["views/default", "views/custom"])
# #
def prepend_view_path(path) def prepend_view_path(path)
@view_paths = superclass.view_paths.dup if @view_paths.nil? @view_paths = superclass.view_paths.dup if !defined?(@view_paths) || @view_paths.nil?
view_paths.unshift(*path) @view_paths.unshift(*path)
ActionView::TemplateFinder.process_view_paths(path)
end end
# Adds a view_path to the end of the view_paths array. # Adds a view_path to the end of the view_paths array.
@ -454,8 +464,7 @@ module ActionController #:nodoc:
# #
def append_view_path(path) def append_view_path(path)
@view_paths = superclass.view_paths.dup if @view_paths.nil? @view_paths = superclass.view_paths.dup if @view_paths.nil?
view_paths.push(*path) @view_paths.push(*path)
ActionView::TemplateFinder.process_view_paths(path)
end end
# Replace sensitive parameter data from the request log. # Replace sensitive parameter data from the request log.
@ -507,38 +516,34 @@ module ActionController #:nodoc:
protected :filter_parameters protected :filter_parameters
end end
# Don't render layouts for templates with the given extensions. delegate :exempt_from_layout, :to => 'ActionView::Base'
def exempt_from_layout(*extensions)
regexps = extensions.collect do |extension|
extension.is_a?(Regexp) ? extension : /\.#{Regexp.escape(extension.to_s)}$/
end
@@exempt_from_layout.merge regexps
end
end end
public public
# Extracts the action_name from the request parameters and performs that action. # Extracts the action_name from the request parameters and performs that action.
def process(request, response, method = :perform_action, *arguments) #:nodoc: def process(request, response, method = :perform_action, *arguments) #:nodoc:
response.request = request
initialize_template_class(response) initialize_template_class(response)
assign_shortcuts(request, response) assign_shortcuts(request, response)
initialize_current_url initialize_current_url
assign_names assign_names
forget_variables_added_to_assigns
log_processing log_processing
send(method, *arguments) send(method, *arguments)
assign_default_content_type_and_charset send_response
response.request = request
response.prepare! unless component_request?
response
ensure ensure
process_cleanup process_cleanup
end end
# Returns a URL that has been rewritten according to the options hash and the defined Routes. def send_response
# (For doing a complete redirect, use redirect_to). response.prepare! unless component_request?
response
end
# Returns a URL that has been rewritten according to the options hash and the defined routes.
# (For doing a complete redirect, use +redirect_to+).
# #
# <tt>url_for</tt> is used to: # <tt>url_for</tt> is used to:
# #
@ -578,7 +583,15 @@ module ActionController #:nodoc:
# missing values in the current request's parameters. Routes attempts to guess when a value should and should not be # missing values in the current request's parameters. Routes attempts to guess when a value should and should not be
# taken from the defaults. There are a few simple rules on how this is performed: # taken from the defaults. There are a few simple rules on how this is performed:
# #
# * If the controller name begins with a slash, no defaults are used: <tt>url_for :controller => '/home'</tt> # * If the controller name begins with a slash no defaults are used:
#
# url_for :controller => '/home'
#
# In particular, a leading slash ensures no namespace is assumed. Thus,
# while <tt>url_for :controller => 'users'</tt> may resolve to
# <tt>Admin::UsersController</tt> if the current controller lives under
# that module, <tt>url_for :controller => '/users'</tt> ensures you link
# to <tt>::UsersController</tt> no matter what.
# * If the controller changes, the action will default to index unless provided # * If the controller changes, the action will default to index unless provided
# #
# The final rule is applied while the URL is being generated and is best illustrated by an example. Let us consider the # The final rule is applied while the URL is being generated and is best illustrated by an example. Let us consider the
@ -648,11 +661,11 @@ module ActionController #:nodoc:
# View load paths for controller. # View load paths for controller.
def view_paths def view_paths
@template.finder.view_paths @template.view_paths
end end
def view_paths=(value) def view_paths=(value)
@template.finder.view_paths = value # Mutex needed @template.view_paths = ActionView::Base.process_view_paths(value)
end end
# Adds a view_path to the front of the view_paths array. # Adds a view_path to the front of the view_paths array.
@ -662,7 +675,7 @@ module ActionController #:nodoc:
# self.prepend_view_path(["views/default", "views/custom"]) # self.prepend_view_path(["views/default", "views/custom"])
# #
def prepend_view_path(path) def prepend_view_path(path)
@template.finder.prepend_view_path(path) # Mutex needed @template.view_paths.unshift(*path)
end end
# Adds a view_path to the end of the view_paths array. # Adds a view_path to the end of the view_paths array.
@ -672,7 +685,7 @@ module ActionController #:nodoc:
# self.append_view_path(["views/default", "views/custom"]) # self.append_view_path(["views/default", "views/custom"])
# #
def append_view_path(path) def append_view_path(path)
@template.finder.append_view_path(path) # Mutex needed @template.view_paths.push(*path)
end end
protected protected
@ -713,6 +726,9 @@ module ActionController #:nodoc:
# # builds the complete response. # # builds the complete response.
# render :partial => "person", :collection => @winners # render :partial => "person", :collection => @winners
# #
# # Renders a collection of partials but with a custom local variable name
# render :partial => "admin_person", :collection => @winners, :as => :person
#
# # Renders the same collection of partials, but also renders the # # Renders the same collection of partials, but also renders the
# # person_divider partial between each person partial. # # person_divider partial between each person partial.
# render :partial => "person", :collection => @winners, :spacer_template => "person_divider" # render :partial => "person", :collection => @winners, :spacer_template => "person_divider"
@ -760,9 +776,6 @@ module ActionController #:nodoc:
# render :file => "/path/to/some/template.erb", :layout => true, :status => 404 # render :file => "/path/to/some/template.erb", :layout => true, :status => 404
# render :file => "c:/path/to/some/template.erb", :layout => true, :status => 404 # render :file => "c:/path/to/some/template.erb", :layout => true, :status => 404
# #
# # Renders a template relative to the template root and chooses the proper file extension
# render :file => "some/template", :use_full_path => true
#
# === Rendering text # === Rendering text
# #
# Rendering of text is usually used for tests or for rendering prepared content, such as a cache. By default, text # Rendering of text is usually used for tests or for rendering prepared content, such as a cache. By default, text
@ -842,7 +855,7 @@ module ActionController #:nodoc:
raise DoubleRenderError, "Can only render or redirect once per action" if performed? raise DoubleRenderError, "Can only render or redirect once per action" if performed?
if options.nil? if options.nil?
return render_for_file(default_template_name, nil, true) return render(:file => default_template_name, :layout => true)
elsif !extra_options.is_a?(Hash) elsif !extra_options.is_a?(Hash)
raise RenderError, "You called render with invalid options : #{options.inspect}, #{extra_options.inspect}" raise RenderError, "You called render with invalid options : #{options.inspect}, #{extra_options.inspect}"
else else
@ -853,6 +866,9 @@ module ActionController #:nodoc:
end end
end end
response.layout = layout = pick_layout(options)
logger.info("Rendering template within #{layout}") if logger && layout
if content_type = options[:content_type] if content_type = options[:content_type]
response.content_type = content_type.to_s response.content_type = content_type.to_s
end end
@ -862,27 +878,21 @@ module ActionController #:nodoc:
end end
if options.has_key?(:text) if options.has_key?(:text)
render_for_text(options[:text], options[:status]) text = layout ? @template.render(options.merge(:text => options[:text], :layout => layout)) : options[:text]
render_for_text(text, options[:status])
else else
if file = options[:file] if file = options[:file]
render_for_file(file, options[:status], options[:use_full_path], options[:locals] || {}) render_for_file(file, options[:status], layout, options[:locals] || {})
elsif template = options[:template] elsif template = options[:template]
render_for_file(template, options[:status], true, options[:locals] || {}) render_for_file(template, options[:status], layout, options[:locals] || {})
elsif inline = options[:inline] elsif inline = options[:inline]
add_variables_to_assigns render_for_text(@template.render(options.merge(:layout => layout)), options[:status])
tmpl = ActionView::InlineTemplate.new(@template, options[:inline], options[:locals], options[:type])
render_for_text(@template.render_template(tmpl), options[:status])
elsif action_name = options[:action] elsif action_name = options[:action]
template = default_template_name(action_name.to_s) render_for_file(default_template_name(action_name.to_s), options[:status], layout)
if options[:layout] && !template_exempt_from_layout?(template)
render_with_a_layout(:file => template, :status => options[:status], :use_full_path => true, :layout => true)
else
render_with_no_layout(:file => template, :status => options[:status], :use_full_path => true)
end
elsif xml = options[:xml] elsif xml = options[:xml]
response.content_type ||= Mime::XML response.content_type ||= Mime::XML
@ -894,36 +904,26 @@ module ActionController #:nodoc:
response.content_type ||= Mime::JSON response.content_type ||= Mime::JSON
render_for_text(json, options[:status]) render_for_text(json, options[:status])
elsif partial = options[:partial] elsif options[:partial]
partial = default_template_name if partial == true options[:partial] = default_template_name if options[:partial] == true
add_variables_to_assigns if layout
render_for_text(@template.render(:text => @template.render(options), :layout => layout), options[:status])
if collection = options[:collection]
render_for_text(
@template.send!(:render_partial_collection, partial, collection,
options[:spacer_template], options[:locals]), options[:status]
)
else else
render_for_text( render_for_text(@template.render(options), options[:status])
@template.send!(:render_partial, partial,
ActionView::Base::ObjectWrapper.new(options[:object]), options[:locals]), options[:status]
)
end end
elsif options[:update] elsif options[:update]
add_variables_to_assigns @template.send(:_evaluate_assigns_and_ivars)
@template.send! :evaluate_assigns
generator = ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.new(@template, &block) generator = ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.new(@template, &block)
response.content_type = Mime::JS response.content_type = Mime::JS
render_for_text(generator.to_s, options[:status]) render_for_text(generator.to_s, options[:status])
elsif options[:nothing] elsif options[:nothing]
# Safari doesn't pass the headers of the return if the response is zero length render_for_text(nil, options[:status])
render_for_text(" ", options[:status])
else else
render_for_file(default_template_name, options[:status], true) render_for_file(default_template_name, options[:status], layout)
end end
end end
end end
@ -934,7 +934,6 @@ module ActionController #:nodoc:
render(options, &block) render(options, &block)
ensure ensure
erase_render_results erase_render_results
forget_variables_added_to_assigns
reset_variables_added_to_assigns reset_variables_added_to_assigns
end end
@ -966,7 +965,6 @@ module ActionController #:nodoc:
render :nothing => true, :status => status render :nothing => true, :status => status
end end
# Clears the rendered results, allowing for another render to be performed. # Clears the rendered results, allowing for another render to be performed.
def erase_render_results #:nodoc: def erase_render_results #:nodoc:
response.body = nil response.body = nil
@ -1051,26 +1049,73 @@ module ActionController #:nodoc:
status = 302 status = 302
end end
response.redirected_to= options
logger.info("Redirected to #{options}") if logger && logger.info?
case options case options
when %r{^\w+://.*} when %r{^\w+://.*}
raise DoubleRenderError if performed? redirect_to_full_url(options, status)
logger.info("Redirected to #{options}") if logger && logger.info?
response.redirect(options, interpret_status(status))
response.redirected_to = options
@performed_redirect = true
when String when String
redirect_to(request.protocol + request.host_with_port + options, :status=>status) redirect_to_full_url(request.protocol + request.host_with_port + options, status)
when :back when :back
request.env["HTTP_REFERER"] ? redirect_to(request.env["HTTP_REFERER"], :status=>status) : raise(RedirectBackError) if referer = request.headers["Referer"]
redirect_to(referer, :status=>status)
when Hash
redirect_to(url_for(options), :status=>status)
response.redirected_to = options
else else
redirect_to(url_for(options), :status=>status) raise RedirectBackError
end
else
redirect_to_full_url(url_for(options), status)
end
end
def redirect_to_full_url(url, status)
raise DoubleRenderError if performed?
response.redirect(url, interpret_status(status))
@performed_redirect = true
end
# Sets the etag and/or last_modified on the response and checks it against
# the client request. If the request doesn't match the options provided, the
# request is considered stale and should be generated from scratch. Otherwise,
# it's fresh and we don't need to generate anything and a reply of "304 Not Modified" is sent.
#
# Example:
#
# def show
# @article = Article.find(params[:id])
#
# if stale?(:etag => @article, :last_modified => @article.created_at.utc)
# @statistics = @article.really_expensive_call
# respond_to do |format|
# # all the supported formats
# end
# end
# end
def stale?(options)
fresh_when(options)
!request.fresh?(response)
end
# Sets the etag, last_modified, or both on the response and renders a
# "304 Not Modified" response if the request is already fresh.
#
# Example:
#
# def show
# @article = Article.find(params[:id])
# fresh_when(:etag => @article, :last_modified => @article.created_at.utc)
# end
#
# This will render the show template if the request isn't sending a matching etag or
# If-Modified-Since header and just a "304 Not Modified" response if there's a match.
def fresh_when(options)
options.assert_valid_keys(:etag, :last_modified)
response.etag = options[:etag] if options[:etag]
response.last_modified = options[:last_modified] if options[:last_modified]
if request.fresh?(response)
head :not_modified
end end
end end
@ -1106,10 +1151,9 @@ module ActionController #:nodoc:
private private
def render_for_file(template_path, status = nil, use_full_path = false, locals = {}) #:nodoc: def render_for_file(template_path, status = nil, layout = nil, locals = {}) #:nodoc:
add_variables_to_assigns
logger.info("Rendering #{template_path}" + (status ? " (#{status})" : '')) if logger logger.info("Rendering #{template_path}" + (status ? " (#{status})" : '')) if logger
render_for_text(@template.render_file(template_path, use_full_path, locals), status) render_for_text @template.render(:file => template_path, :locals => locals, :layout => layout), status
end end
def render_for_text(text = nil, status = nil, append_response = false) #:nodoc: def render_for_text(text = nil, status = nil, append_response = false) #:nodoc:
@ -1121,13 +1165,17 @@ module ActionController #:nodoc:
response.body ||= '' response.body ||= ''
response.body << text.to_s response.body << text.to_s
else else
response.body = text.is_a?(Proc) ? text : text.to_s response.body = case text
when Proc then text
when nil then " " # Safari doesn't pass the headers of the return if the response is zero length
else text.to_s
end
end end
end end
def initialize_template_class(response) def initialize_template_class(response)
response.template = ActionView::Base.new(self.class.view_paths, {}, self) response.template = ActionView::Base.new(self.class.view_paths, {}, self)
response.template.extend self.class.master_helper_module response.template.helpers.send :include, self.class.master_helper_module
response.redirected_to = nil response.redirected_to = nil
@performed_render = @performed_redirect = false @performed_render = @performed_redirect = false
end end
@ -1140,7 +1188,6 @@ module ActionController #:nodoc:
@_session = @_response.session @_session = @_response.session
@template = @_response.template @template = @_response.template
@assigns = @_response.template.assigns
@_headers = @_response.headers @_headers = @_response.headers
end end
@ -1162,16 +1209,16 @@ module ActionController #:nodoc:
end end
def perform_action def perform_action
if self.class.action_methods.include?(action_name) if action_methods.include?(action_name)
send(action_name) send(action_name)
default_render unless performed? default_render unless performed?
elsif respond_to? :method_missing elsif respond_to? :method_missing
method_missing action_name method_missing action_name
default_render unless performed? default_render unless performed?
elsif template_exists? && template_public? elsif template_exists?
default_render default_render
else else
raise UnknownAction, "No action responded to #{action_name}", caller raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence}", caller
end end
end end
@ -1184,43 +1231,30 @@ module ActionController #:nodoc:
end end
def assign_default_content_type_and_charset def assign_default_content_type_and_charset
response.content_type ||= Mime::HTML response.assign_default_content_type_and_charset!
response.charset ||= self.class.default_charset unless sending_file?
end
def sending_file?
response.headers["Content-Transfer-Encoding"] == "binary"
end end
deprecate :assign_default_content_type_and_charset => :'response.assign_default_content_type_and_charset!'
def action_methods def action_methods
self.class.action_methods self.class.action_methods
end end
def self.action_methods def self.action_methods
@action_methods ||= Set.new(public_instance_methods.map(&:to_s)) - hidden_actions @action_methods ||=
end # All public instance methods of this class, including ancestors
public_instance_methods(true).map { |m| m.to_s }.to_set -
def add_variables_to_assigns # Except for public instance methods of Base and its ancestors
unless @variables_added Base.public_instance_methods(true).map { |m| m.to_s } +
add_instance_variables_to_assigns # Be sure to include shadowed public instance methods of this class
@variables_added = true public_instance_methods(false).map { |m| m.to_s } -
end # And always exclude explicitly hidden actions
end hidden_actions
def forget_variables_added_to_assigns
@variables_added = nil
end end
def reset_variables_added_to_assigns def reset_variables_added_to_assigns
@template.instance_variable_set("@assigns_added", nil) @template.instance_variable_set("@assigns_added", nil)
end end
def add_instance_variables_to_assigns
(instance_variable_names - @@protected_view_variables).each do |var|
@assigns[var[1..-1]] = instance_variable_get(var)
end
end
def request_origin def request_origin
# this *needs* to be cached! # this *needs* to be cached!
# otherwise you'd get different results if calling it more than once # otherwise you'd get different results if calling it more than once
@ -1236,17 +1270,9 @@ module ActionController #:nodoc:
end end
def template_exists?(template_name = default_template_name) def template_exists?(template_name = default_template_name)
@template.finder.file_exists?(template_name) @template.send(:_pick_template, template_name) ? true : false
end rescue ActionView::MissingTemplate
false
def template_public?(template_name = default_template_name)
@template.file_public?(template_name)
end
def template_exempt_from_layout?(template_name = default_template_name)
extension = @template && @template.finder.pick_template_extension(template_name)
name_with_extension = !template_name.include?('.') && extension ? "#{template_name}.#{extension}" : template_name
@@exempt_from_layout.any? { |ext| name_with_extension =~ ext }
end end
def default_template_name(action_name = self.action_name) def default_template_name(action_name = self.action_name)
@ -1256,7 +1282,7 @@ module ActionController #:nodoc:
action_name = strip_out_controller(action_name) action_name = strip_out_controller(action_name)
end end
end end
"#{self.class.controller_path}/#{action_name}" "#{self.controller_path}/#{action_name}"
end end
def strip_out_controller(path) def strip_out_controller(path)
@ -1264,7 +1290,7 @@ module ActionController #:nodoc:
end end
def template_path_includes_controller?(path) def template_path_includes_controller?(path)
self.class.controller_path.split('/')[-1] == path.split('/')[0] self.controller_path.split('/')[-1] == path.split('/')[0]
end end
def process_cleanup def process_cleanup

View file

@ -24,7 +24,7 @@ module ActionController #:nodoc:
if logger && logger.level == log_level if logger && logger.level == log_level
result = nil result = nil
seconds = Benchmark.realtime { result = use_silence ? silence { yield } : yield } seconds = Benchmark.realtime { result = use_silence ? silence { yield } : yield }
logger.add(log_level, "#{title} (#{'%.5f' % seconds})") logger.add(log_level, "#{title} (#{('%.1f' % (seconds * 1000))}ms)")
result result
else else
yield yield
@ -42,53 +42,66 @@ module ActionController #:nodoc:
protected protected
def render_with_benchmark(options = nil, extra_options = {}, &block) def render_with_benchmark(options = nil, extra_options = {}, &block)
unless logger if logger
render_without_benchmark(options, extra_options, &block) if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
else db_runtime = ActiveRecord::Base.connection.reset_runtime
db_runtime = ActiveRecord::Base.connection.reset_runtime if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected? end
render_output = nil render_output = nil
@rendering_runtime = Benchmark::realtime{ render_output = render_without_benchmark(options, extra_options, &block) } @view_runtime = Benchmark::realtime { render_output = render_without_benchmark(options, extra_options, &block) }
if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected? if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
@db_rt_before_render = db_runtime @db_rt_before_render = db_runtime
@db_rt_after_render = ActiveRecord::Base.connection.reset_runtime @db_rt_after_render = ActiveRecord::Base.connection.reset_runtime
@rendering_runtime -= @db_rt_after_render @view_runtime -= @db_rt_after_render
end end
render_output render_output
else
render_without_benchmark(options, extra_options, &block)
end end
end end
private private
def perform_action_with_benchmark def perform_action_with_benchmark
unless logger if logger
perform_action_without_benchmark seconds = [ Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001 ].max
else logging_view = defined?(@view_runtime)
runtime = [ Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001 ].max logging_active_record = Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
log_message = "Completed in #{sprintf("%.0f", seconds * 1000)}ms"
if logging_view || logging_active_record
log_message << " ("
log_message << view_runtime if logging_view
if logging_active_record
log_message << ", " if logging_view
log_message << active_record_runtime + ")"
else
")"
end
end
log_message = "Completed in #{sprintf("%.5f", runtime)} (#{(1 / runtime).floor} reqs/sec)"
log_message << rendering_runtime(runtime) if defined?(@rendering_runtime)
log_message << active_record_runtime(runtime) if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
log_message << " | #{headers["Status"]}" log_message << " | #{headers["Status"]}"
log_message << " [#{complete_request_uri rescue "unknown"}]" log_message << " [#{complete_request_uri rescue "unknown"}]"
logger.info(log_message) logger.info(log_message)
response.headers["X-Runtime"] = sprintf("%.5f", runtime) response.headers["X-Runtime"] = "#{sprintf("%.0f", seconds * 1000)}ms"
else
perform_action_without_benchmark
end end
end end
def rendering_runtime(runtime) def view_runtime
percentage = @rendering_runtime * 100 / runtime "View: %.0f" % (@view_runtime * 1000)
" | Rendering: %.5f (%d%%)" % [@rendering_runtime, percentage.to_i]
end end
def active_record_runtime(runtime) def active_record_runtime
db_runtime = ActiveRecord::Base.connection.reset_runtime db_runtime = ActiveRecord::Base.connection.reset_runtime
db_runtime += @db_rt_before_render if @db_rt_before_render db_runtime += @db_rt_before_render if @db_rt_before_render
db_runtime += @db_rt_after_render if @db_rt_after_render db_runtime += @db_rt_after_render if @db_rt_after_render
db_percentage = db_runtime * 100 / runtime "DB: %.0f" % (db_runtime * 1000)
" | DB: %.5f (%d%%)" % [db_runtime, db_percentage.to_i]
end end
end end
end end

View file

@ -27,19 +27,23 @@ module ActionController #:nodoc:
# You can set modify the default action cache path by passing a :cache_path option. This will be passed directly to ActionCachePath.path_for. This is handy # You can set modify the default action cache path by passing a :cache_path option. This will be passed directly to ActionCachePath.path_for. This is handy
# for actions with multiple possible routes that should be cached differently. If a block is given, it is called with the current controller instance. # for actions with multiple possible routes that should be cached differently. If a block is given, it is called with the current controller instance.
# #
# And you can also use :if to pass a Proc that specifies when the action should be cached. # And you can also use :if (or :unless) to pass a Proc that specifies when the action should be cached.
#
# Finally, if you are using memcached, you can also pass :expires_in.
# #
# class ListsController < ApplicationController # class ListsController < ApplicationController
# before_filter :authenticate, :except => :public # before_filter :authenticate, :except => :public
# caches_page :public # caches_page :public
# caches_action :index, :if => Proc.new { |c| !c.request.format.json? } # cache if is not a JSON request # caches_action :index, :if => Proc.new { |c| !c.request.format.json? } # cache if is not a JSON request
# caches_action :show, :cache_path => { :project => 1 } # caches_action :show, :cache_path => { :project => 1 }, :expires_in => 1.hour
# caches_action :feed, :cache_path => Proc.new { |controller| # caches_action :feed, :cache_path => Proc.new { |controller|
# controller.params[:user_id] ? # controller.params[:user_id] ?
# controller.send(:user_list_url, c.params[:user_id], c.params[:id]) : # controller.send(:user_list_url, controller.params[:user_id], controller.params[:id]) :
# controller.send(:list_url, c.params[:id]) } # controller.send(:list_url, controller.params[:id]) }
# end # end
# #
# If you pass :layout => false, it will only cache your action content. It is useful when your layout has dynamic information.
#
module Actions module Actions
def self.included(base) #:nodoc: def self.included(base) #:nodoc:
base.extend(ClassMethods) base.extend(ClassMethods)
@ -54,7 +58,10 @@ module ActionController #:nodoc:
def caches_action(*actions) def caches_action(*actions)
return unless cache_configured? return unless cache_configured?
options = actions.extract_options! options = actions.extract_options!
around_filter(ActionCacheFilter.new(:cache_path => options.delete(:cache_path)), {:only => actions}.merge(options)) filter_options = { :only => actions, :if => options.delete(:if), :unless => options.delete(:unless) }
cache_filter = ActionCacheFilter.new(:layout => options.delete(:layout), :cache_path => options.delete(:cache_path), :store_options => options)
around_filter(cache_filter, filter_options)
end end
end end
@ -64,10 +71,10 @@ module ActionController #:nodoc:
if options[:action].is_a?(Array) if options[:action].is_a?(Array)
options[:action].dup.each do |action| options[:action].dup.each do |action|
expire_fragment(ActionCachePath.path_for(self, options.merge({ :action => action }))) expire_fragment(ActionCachePath.path_for(self, options.merge({ :action => action }), false))
end end
else else
expire_fragment(ActionCachePath.path_for(self, options)) expire_fragment(ActionCachePath.path_for(self, options, false))
end end
end end
@ -77,11 +84,13 @@ module ActionController #:nodoc:
end end
def before(controller) def before(controller)
cache_path = ActionCachePath.new(controller, path_options_for(controller, @options)) cache_path = ActionCachePath.new(controller, path_options_for(controller, @options.slice(:cache_path)))
if cache = controller.read_fragment(cache_path.path) if cache = controller.read_fragment(cache_path.path, @options[:store_options])
controller.rendered_action_cache = true controller.rendered_action_cache = true
set_content_type!(controller, cache_path.extension) set_content_type!(controller, cache_path.extension)
controller.send!(:render_for_text, cache) options = { :text => cache }
options.merge!(:layout => true) if cache_layout?
controller.__send__(:render, options)
false false
else else
controller.action_cache_path = cache_path controller.action_cache_path = cache_path
@ -90,7 +99,8 @@ module ActionController #:nodoc:
def after(controller) def after(controller)
return if controller.rendered_action_cache || !caching_allowed(controller) return if controller.rendered_action_cache || !caching_allowed(controller)
controller.write_fragment(controller.action_cache_path.path, controller.response.body) action_content = cache_layout? ? content_for_layout(controller) : controller.response.body
controller.write_fragment(controller.action_cache_path.path, action_content, @options[:store_options])
end end
private private
@ -105,22 +115,38 @@ module ActionController #:nodoc:
def caching_allowed(controller) def caching_allowed(controller)
controller.request.get? && controller.response.headers['Status'].to_i == 200 controller.request.get? && controller.response.headers['Status'].to_i == 200
end end
def cache_layout?
@options[:layout] == false
end
def content_for_layout(controller)
controller.response.layout && controller.response.template.instance_variable_get('@cached_content_for_layout')
end
end end
class ActionCachePath class ActionCachePath
attr_reader :path, :extension attr_reader :path, :extension
class << self class << self
def path_for(controller, options) def path_for(controller, options, infer_extension=true)
new(controller, options).path new(controller, options, infer_extension).path
end end
end end
def initialize(controller, options = {}) # When true, infer_extension will look up the cache path extension from the request's path & format.
@extension = extract_extension(controller.request.path) # This is desirable when reading and writing the cache, but not when expiring the cache - expire_action should expire the same files regardless of the request format.
def initialize(controller, options = {}, infer_extension=true)
if infer_extension and options.is_a? Hash
request_extension = extract_extension(controller.request)
options = options.reverse_merge(:format => request_extension)
end
path = controller.url_for(options).split('://').last path = controller.url_for(options).split('://').last
normalize!(path) normalize!(path)
if infer_extension
@extension = request_extension
add_extension!(path, @extension) add_extension!(path, @extension)
end
@path = URI.unescape(path) @path = URI.unescape(path)
end end
@ -130,13 +156,19 @@ module ActionController #:nodoc:
end end
def add_extension!(path, extension) def add_extension!(path, extension)
path << ".#{extension}" if extension path << ".#{extension}" if extension and !path.ends_with?(extension)
end end
def extract_extension(file_path) def extract_extension(request)
# Don't want just what comes after the last '.' to accommodate multi part extensions # Don't want just what comes after the last '.' to accommodate multi part extensions
# such as tar.gz. # such as tar.gz.
file_path[/^[^.]+\.(.+)$/, 1] extension = request.path[/^[^.]+\.(.+)$/, 1]
# If there's no extension in the path, check request.format
if extension.nil?
extension = request.cache_format
end
extension
end end
end end
end end

View file

@ -2,7 +2,7 @@ module ActionController #:nodoc:
module Caching module Caching
# Fragment caching is used for caching various blocks within templates without caching the entire action as a whole. This is useful when # Fragment caching is used for caching various blocks within templates without caching the entire action as a whole. This is useful when
# certain elements of an action change frequently or depend on complicated state while other parts rarely change or can be shared amongst multiple # certain elements of an action change frequently or depend on complicated state while other parts rarely change or can be shared amongst multiple
# parties. The caching is doing using the cache helper available in the Action View. A template with caching might look something like: # parties. The caching is done using the cache helper available in the Action View. A template with caching might look something like:
# #
# <b>Hello <%= @name %></b> # <b>Hello <%= @name %></b>
# <% cache do %> # <% cache do %>
@ -26,32 +26,6 @@ module ActionController #:nodoc:
# #
# expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics") # expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics")
module Fragments module Fragments
def self.included(base) #:nodoc:
base.class_eval do
class << self
def fragment_cache_store=(store_option) #:nodoc:
ActiveSupport::Deprecation.warn('The fragment_cache_store= method is now use cache_store=')
self.cache_store = store_option
end
def fragment_cache_store #:nodoc:
ActiveSupport::Deprecation.warn('The fragment_cache_store method is now use cache_store')
cache_store
end
end
def fragment_cache_store=(store_option) #:nodoc:
ActiveSupport::Deprecation.warn('The fragment_cache_store= method is now use cache_store=')
self.cache_store = store_option
end
def fragment_cache_store #:nodoc:
ActiveSupport::Deprecation.warn('The fragment_cache_store method is now use cache_store')
cache_store
end
end
end
# Given a key (as described in <tt>expire_fragment</tt>), returns a key suitable for use in reading, # Given a key (as described in <tt>expire_fragment</tt>), returns a key suitable for use in reading,
# writing, or expiring a cached fragment. If the key is a hash, the generated key is the return # writing, or expiring a cached fragment. If the key is a hash, the generated key is the return
# value of url_for on that hash (without the protocol). All keys are prefixed with "views/" and uses # value of url_for on that hash (without the protocol). All keys are prefixed with "views/" and uses
@ -60,11 +34,8 @@ module ActionController #:nodoc:
ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views) ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views)
end end
def fragment_for(block, name = {}, options = nil) #:nodoc: def fragment_for(buffer, name = {}, options = nil, &block) #:nodoc:
unless perform_caching then block.call; return end if perform_caching
buffer = yield
if cache = read_fragment(name, options) if cache = read_fragment(name, options)
buffer.concat(cache) buffer.concat(cache)
else else
@ -72,6 +43,9 @@ module ActionController #:nodoc:
block.call block.call
write_fragment(name, buffer[pos..-1], options) write_fragment(name, buffer[pos..-1], options)
end end
else
block.call
end
end end
# Writes <tt>content</tt> to the location signified by <tt>key</tt> (see <tt>expire_fragment</tt> for acceptable formats) # Writes <tt>content</tt> to the location signified by <tt>key</tt> (see <tt>expire_fragment</tt> for acceptable formats)

View file

@ -83,13 +83,13 @@ module ActionController #:nodoc:
controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}" controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}" action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
send!(controller_callback_method_name) if respond_to?(controller_callback_method_name, true) __send__(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
send!(action_callback_method_name) if respond_to?(action_callback_method_name, true) __send__(action_callback_method_name) if respond_to?(action_callback_method_name, true)
end end
def method_missing(method, *arguments) def method_missing(method, *arguments)
return if @controller.nil? return if @controller.nil?
@controller.send!(method, *arguments) @controller.__send__(method, *arguments)
end end
end end
end end

View file

@ -6,28 +6,8 @@ class CGI #:nodoc:
# * Expose the CGI instance to session stores. # * Expose the CGI instance to session stores.
# * Don't require 'digest/md5' whenever a new session id is generated. # * Don't require 'digest/md5' whenever a new session id is generated.
class Session #:nodoc: class Session #:nodoc:
begin
require 'securerandom'
# Generate a 32-character unique id using SecureRandom.
# This is used to generate session ids but may be reused elsewhere.
def self.generate_unique_id(constant = nil) def self.generate_unique_id(constant = nil)
SecureRandom.hex(16) ActiveSupport::SecureRandom.hex(16)
end
rescue LoadError
# Generate an 32-character unique id based on a hash of the current time,
# a random number, the process id, and a constant string. This is used
# to generate session ids but may be reused elsewhere.
def self.generate_unique_id(constant = 'foobar')
md5 = Digest::MD5.new
now = Time.now
md5 << now.to_s
md5 << String(now.usec)
md5 << String(rand(0))
md5 << String($$)
md5 << constant
md5.hexdigest
end
end end
# Make the CGI instance available to session stores. # Make the CGI instance available to session stores.

View file

@ -42,13 +42,14 @@ module ActionController #:nodoc:
:prefix => "ruby_sess.", # prefix session file names :prefix => "ruby_sess.", # prefix session file names
:session_path => "/", # available to all paths in app :session_path => "/", # available to all paths in app
:session_key => "_session_id", :session_key => "_session_id",
:cookie_only => true :cookie_only => true,
} unless const_defined?(:DEFAULT_SESSION_OPTIONS) :session_http_only=> true
}
def initialize(cgi, session_options = {}) def initialize(cgi, session_options = {})
@cgi = cgi @cgi = cgi
@session_options = session_options @session_options = session_options
@env = @cgi.send!(:env_table) @env = @cgi.__send__(:env_table)
super() super()
end end
@ -61,53 +62,14 @@ module ActionController #:nodoc:
end end
end end
# The request body is an IO input stream. If the RAW_POST_DATA environment def body_stream #:nodoc:
# variable is already set, wrap it in a StringIO.
def body
if raw_post = env['RAW_POST_DATA']
raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding)
StringIO.new(raw_post)
else
@cgi.stdinput @cgi.stdinput
end end
end
def query_parameters
@query_parameters ||= self.class.parse_query_parameters(query_string)
end
def request_parameters
@request_parameters ||= parse_formatted_request_parameters
end
def cookies def cookies
@cgi.cookies.freeze @cgi.cookies.freeze
end end
def host_with_port_without_standard_port_handling
if forwarded = env["HTTP_X_FORWARDED_HOST"]
forwarded.split(/,\s?/).last
elsif http_host = env['HTTP_HOST']
http_host
elsif server_name = env['SERVER_NAME']
server_name
else
"#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
end
end
def host
host_with_port_without_standard_port_handling.sub(/:\d+$/, '')
end
def port
if host_with_port_without_standard_port_handling =~ /:(\d+)$/
$1.to_i
else
standard_port
end
end
def session def session
unless defined?(@session) unless defined?(@session)
if @session_options == false if @session_options == false
@ -146,7 +108,7 @@ module ActionController #:nodoc:
end end
def method_missing(method_id, *arguments) def method_missing(method_id, *arguments)
@cgi.send!(method_id, *arguments) rescue super @cgi.__send__(method_id, *arguments) rescue super
end end
private private
@ -203,7 +165,7 @@ end_msg
begin begin
output.write(@cgi.header(@headers)) output.write(@cgi.header(@headers))
if @cgi.send!(:env_table)['REQUEST_METHOD'] == 'HEAD' if @cgi.__send__(:env_table)['REQUEST_METHOD'] == 'HEAD'
return return
elsif @body.respond_to?(:call) elsif @body.respond_to?(:call)
# Flush the output now in case the @body Proc uses # Flush the output now in case the @body Proc uses

View file

@ -38,6 +38,7 @@ module ActionController #:nodoc:
def self.included(base) #:nodoc: def self.included(base) #:nodoc:
base.class_eval do base.class_eval do
include InstanceMethods include InstanceMethods
include ActiveSupport::Deprecation
extend ClassMethods extend ClassMethods
helper HelperMethods helper HelperMethods
@ -64,7 +65,7 @@ module ActionController #:nodoc:
module HelperMethods module HelperMethods
def render_component(options) def render_component(options)
@controller.send!(:render_component_as_string, options) @controller.__send__(:render_component_as_string, options)
end end
end end
@ -82,6 +83,7 @@ module ActionController #:nodoc:
render_for_text(component_response(options, true).body, response.headers["Status"]) render_for_text(component_response(options, true).body, response.headers["Status"])
end end
end end
deprecate :render_component => "Please install render_component plugin from http://github.com/rails/render_component/tree/master"
# Returns the component response as a string # Returns the component response as a string
def render_component_as_string(options) #:doc: def render_component_as_string(options) #:doc:
@ -95,6 +97,7 @@ module ActionController #:nodoc:
end end
end end
end end
deprecate :render_component_as_string => "Please install render_component plugin from http://github.com/rails/render_component/tree/master"
def flash_with_components(refresh = false) #:nodoc: def flash_with_components(refresh = false) #:nodoc:
if !defined?(@_flash) || refresh if !defined?(@_flash) || refresh

View file

@ -22,6 +22,16 @@ module ActionController #:nodoc:
# #
# cookies.delete :user_name # cookies.delete :user_name
# #
# Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie:
#
# cookies[:key] = {
# :value => 'a yummy cookie',
# :expires => 1.year.from_now,
# :domain => 'domain.com'
# }
#
# cookies.delete(:key, :domain => 'domain.com')
#
# The option symbols for setting cookies are: # The option symbols for setting cookies are:
# #
# * <tt>:value</tt> - The cookie's value or list of values (as an array). # * <tt>:value</tt> - The cookie's value or list of values (as an array).

View file

@ -22,11 +22,12 @@ module ActionController
end end
if defined?(ActiveRecord) if defined?(ActiveRecord)
after_dispatch :checkin_connections
before_dispatch { ActiveRecord::Base.verify_active_connections! } before_dispatch { ActiveRecord::Base.verify_active_connections! }
to_prepare(:activerecord_instantiate_observers) { ActiveRecord::Base.instantiate_observers } to_prepare(:activerecord_instantiate_observers) { ActiveRecord::Base.instantiate_observers }
end end
after_dispatch :flush_logger if defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER.respond_to?(:flush) after_dispatch :flush_logger if Base.logger && Base.logger.respond_to?(:flush)
end end
# Backward-compatible class method takes CGI-specific args. Deprecated # Backward-compatible class method takes CGI-specific args. Deprecated
@ -46,7 +47,7 @@ module ActionController
def to_prepare(identifier = nil, &block) def to_prepare(identifier = nil, &block)
@prepare_dispatch_callbacks ||= ActiveSupport::Callbacks::CallbackChain.new @prepare_dispatch_callbacks ||= ActiveSupport::Callbacks::CallbackChain.new
callback = ActiveSupport::Callbacks::Callback.new(:prepare_dispatch, block, :identifier => identifier) callback = ActiveSupport::Callbacks::Callback.new(:prepare_dispatch, block, :identifier => identifier)
@prepare_dispatch_callbacks | callback @prepare_dispatch_callbacks.replace_or_append!(callback)
end end
# If the block raises, send status code as a last-ditch response. # If the block raises, send status code as a last-ditch response.
@ -96,12 +97,11 @@ module ActionController
include ActiveSupport::Callbacks include ActiveSupport::Callbacks
define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch
def initialize(output, request = nil, response = nil) def initialize(output = $stdout, request = nil, response = nil)
@output, @request, @response = output, request, response @output, @request, @response = output, request, response
end end
def dispatch def dispatch_unlocked
@@guard.synchronize do
begin begin
run_callbacks :before_dispatch run_callbacks :before_dispatch
handle_request handle_request
@ -111,6 +111,15 @@ module ActionController
run_callbacks :after_dispatch, :enumerator => :reverse_each run_callbacks :after_dispatch, :enumerator => :reverse_each
end end
end end
def dispatch
if ActionController::Base.allow_concurrency
dispatch_unlocked
else
@@guard.synchronize do
dispatch_unlocked
end
end
end end
def dispatch_cgi(cgi, session_options) def dispatch_cgi(cgi, session_options)
@ -123,12 +132,19 @@ module ActionController
failsafe_rescue exception failsafe_rescue exception
end end
def call(env)
@request = RackRequest.new(env)
@response = RackResponse.new(@request)
dispatch
end
def reload_application def reload_application
# Run prepare callbacks before every request in development mode # Run prepare callbacks before every request in development mode
run_callbacks :prepare_dispatch run_callbacks :prepare_dispatch
Routing::Routes.reload Routing::Routes.reload
ActionView::TemplateFinder.reload! unless ActionView::Base.cache_template_loading ActionController::Base.view_paths.reload!
ActionView::Helpers::AssetTagHelper::AssetTag::Cache.clear
end end
# Cleanup the application by clearing out loaded classes so they can # Cleanup the application by clearing out loaded classes so they can
@ -140,7 +156,22 @@ module ActionController
end end
def flush_logger def flush_logger
RAILS_DEFAULT_LOGGER.flush Base.logger.flush
end
def mark_as_test_request!
@test_request = true
self
end
def test_request?
@test_request
end
def checkin_connections
# Don't return connection (and peform implicit rollback) if this request is a part of integration test
return if test_request?
ActiveRecord::Base.clear_active_connections!
end end
protected protected

View file

@ -94,7 +94,7 @@ module ActionController #:nodoc:
map! do |filter| map! do |filter|
if filters.include?(filter) if filters.include?(filter)
new_filter = filter.dup new_filter = filter.dup
new_filter.options.merge!(options) new_filter.update_options!(options)
new_filter new_filter
else else
filter filter
@ -104,16 +104,34 @@ module ActionController #:nodoc:
end end
class Filter < ActiveSupport::Callbacks::Callback #:nodoc: class Filter < ActiveSupport::Callbacks::Callback #:nodoc:
def initialize(kind, method, options = {})
super
update_options! options
end
# override these to return true in appropriate subclass
def before? def before?
self.class == BeforeFilter false
end end
def after? def after?
self.class == AfterFilter false
end end
def around? def around?
self.class == AroundFilter false
end
# Make sets of strings from :only/:except options
def update_options!(other)
if other
convert_only_and_except_options_to_sets_of_strings(other)
if other[:skip]
convert_only_and_except_options_to_sets_of_strings(other[:skip])
end
end
options.update(other)
end end
private private
@ -127,9 +145,9 @@ module ActionController #:nodoc:
def included_in_action?(controller, options) def included_in_action?(controller, options)
if options[:only] if options[:only]
Array(options[:only]).map(&:to_s).include?(controller.action_name) options[:only].include?(controller.action_name)
elsif options[:except] elsif options[:except]
!Array(options[:except]).map(&:to_s).include?(controller.action_name) !options[:except].include?(controller.action_name)
else else
true true
end end
@ -138,6 +156,14 @@ module ActionController #:nodoc:
def should_run_callback?(controller) def should_run_callback?(controller)
should_not_skip?(controller) && included_in_action?(controller, options) && super should_not_skip?(controller) && included_in_action?(controller, options) && super
end end
def convert_only_and_except_options_to_sets_of_strings(opts)
[:only, :except].each do |key|
if values = opts[key]
opts[key] = Array(values).map(&:to_s).to_set
end
end
end
end end
class AroundFilter < Filter #:nodoc: class AroundFilter < Filter #:nodoc:
@ -145,6 +171,10 @@ module ActionController #:nodoc:
:around :around
end end
def around?
true
end
def call(controller, &block) def call(controller, &block)
if should_run_callback?(controller) if should_run_callback?(controller)
method = filter_responds_to_before_and_after? ? around_proc : self.method method = filter_responds_to_before_and_after? ? around_proc : self.method
@ -169,8 +199,8 @@ module ActionController #:nodoc:
Proc.new do |controller, action| Proc.new do |controller, action|
method.before(controller) method.before(controller)
if controller.send!(:performed?) if controller.__send__(:performed?)
controller.send!(:halt_filter_chain, method, :rendered_or_redirected) controller.__send__(:halt_filter_chain, method, :rendered_or_redirected)
else else
begin begin
action.call action.call
@ -187,10 +217,14 @@ module ActionController #:nodoc:
:before :before
end end
def before?
true
end
def call(controller, &block) def call(controller, &block)
super super
if controller.send!(:performed?) if controller.__send__(:performed?)
controller.send!(:halt_filter_chain, method, :rendered_or_redirected) controller.__send__(:halt_filter_chain, method, :rendered_or_redirected)
end end
end end
end end
@ -199,6 +233,10 @@ module ActionController #:nodoc:
def type def type
:after :after
end end
def after?
true
end
end end
# Filters enable controllers to run shared pre- and post-processing code for its actions. These filters can be used to do # Filters enable controllers to run shared pre- and post-processing code for its actions. These filters can be used to do

View file

@ -1,13 +1,16 @@
require 'active_support/memoizable'
module ActionController module ActionController
module Http module Http
class Headers < ::Hash class Headers < ::Hash
extend ActiveSupport::Memoizable
def initialize(constructor = {}) def initialize(*args)
if constructor.is_a?(Hash) if args.size == 1 && args[0].is_a?(Hash)
super() super()
update(constructor) update(args[0])
else else
super(constructor) super
end end
end end
@ -15,17 +18,16 @@ module ActionController
if include?(header_name) if include?(header_name)
super super
else else
super(normalize_header(header_name)) super(env_name(header_name))
end end
end end
private private
# Takes an HTTP header name and returns it in the # Converts a HTTP header name to an environment variable name.
# format def env_name(header_name)
def normalize_header(header_name)
"HTTP_#{header_name.upcase.gsub(/-/, '_')}" "HTTP_#{header_name.upcase.gsub(/-/, '_')}"
end end
memoize :env_name
end end
end end
end end

View file

@ -204,8 +204,8 @@ module ActionController #:nodoc:
begin begin
child.master_helper_module = Module.new child.master_helper_module = Module.new
child.master_helper_module.send! :include, master_helper_module child.master_helper_module.__send__ :include, master_helper_module
child.send! :default_helper_module! child.__send__ :default_helper_module!
rescue MissingSourceFile => e rescue MissingSourceFile => e
raise unless e.is_missing?("helpers/#{child.controller_path}_helper") raise unless e.is_missing?("helpers/#{child.controller_path}_helper")
end end

View file

@ -117,7 +117,7 @@ module ActionController
def authentication_request(controller, realm) def authentication_request(controller, realm)
controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}") controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}")
controller.send! :render, :text => "HTTP Basic: Access denied.\n", :status => :unauthorized controller.__send__ :render, :text => "HTTP Basic: Access denied.\n", :status => :unauthorized
end end
end end
end end

View file

@ -1,9 +1,10 @@
require 'stringio' require 'active_support/test_case'
require 'uri'
require 'action_controller/dispatcher' require 'action_controller/dispatcher'
require 'action_controller/test_process' require 'action_controller/test_process'
require 'stringio'
require 'uri'
module ActionController module ActionController
module Integration #:nodoc: module Integration #:nodoc:
# An integration Session instance represents a set of requests and responses # An integration Session instance represents a set of requests and responses
@ -100,7 +101,7 @@ module ActionController
@https = flag @https = flag
end end
# Return +true+ if the session is mimicing a secure HTTPS request. # Return +true+ if the session is mimicking a secure HTTPS request.
# #
# if session.https? # if session.https?
# ... # ...
@ -164,11 +165,19 @@ module ActionController
status/100 == 3 status/100 == 3
end end
# Performs a GET request with the given parameters. The parameters may # Performs a GET request with the given parameters.
# be +nil+, a Hash, or a string that is appropriately encoded #
# - +path+: The URI (as a String) on which you want to perform a GET request.
# - +parameters+: The HTTP parameters that you want to pass. This may be +nil+,
# a Hash, or a String that is appropriately encoded
# (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>). # (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
# The headers should be a hash. The keys will automatically be upcased, with the # - +headers+: Additional HTTP headers to pass, as a Hash. The keys will
# prefix 'HTTP_' added if needed. # automatically be upcased, with the prefix 'HTTP_' added if needed.
#
# This method returns an AbstractResponse object, which one can use to inspect
# the details of the response. Furthermore, if this method was called from an
# ActionController::IntegrationTest object, then that object's <tt>@response</tt>
# instance variable will point to the same response object.
# #
# You can also perform POST, PUT, DELETE, and HEAD requests with +post+, # You can also perform POST, PUT, DELETE, and HEAD requests with +post+,
# +put+, +delete+, and +head+. # +put+, +delete+, and +head+.
@ -219,21 +228,6 @@ module ActionController
end end
private private
class StubCGI < CGI #:nodoc:
attr_accessor :stdinput, :stdoutput, :env_table
def initialize(env, stdinput = nil)
self.env_table = env
self.stdoutput = StringIO.new
super
stdinput.set_encoding(Encoding::BINARY) if stdinput.respond_to?(:set_encoding)
stdinput.force_encoding(Encoding::BINARY) if stdinput.respond_to?(:force_encoding)
@stdinput = stdinput.is_a?(IO) ? stdinput : StringIO.new(stdinput || '')
end
end
# Tailors the session based on the given URI, setting the HTTPS value # Tailors the session based on the given URI, setting the HTTPS value
# and the hostname. # and the hostname.
def interpret_uri(path) def interpret_uri(path)
@ -281,9 +275,8 @@ module ActionController
ActionController::Base.clear_last_instantiation! ActionController::Base.clear_last_instantiation!
cgi = StubCGI.new(env, data) env['rack.input'] = data.is_a?(IO) ? data : StringIO.new(data || '')
ActionController::Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, cgi.stdoutput) @status, @headers, result_body = ActionController::Dispatcher.new.mark_as_test_request!.call(env)
@result = cgi.stdoutput.string
@request_count += 1 @request_count += 1
@controller = ActionController::Base.last_instantiation @controller = ActionController::Base.last_instantiation
@ -297,7 +290,29 @@ module ActionController
@html_document = nil @html_document = nil
parse_result # Inject status back in for backwords compatibility with CGI
@headers['Status'] = @status
@status, @status_message = @status.split(/ /)
@status = @status.to_i
cgi_headers = Hash.new { |h,k| h[k] = [] }
@headers.each do |key, value|
cgi_headers[key.downcase] << value
end
cgi_headers['set-cookie'] = cgi_headers['set-cookie'].first
@headers = cgi_headers
@response.headers['cookie'] ||= []
(@headers['set-cookie'] || []).each do |cookie|
name, value = cookie.match(/^([^=]*)=([^;]*);/)[1,2]
@cookies[name] = value
# Fake CGI cookie header
# DEPRECATE: Use response.headers["Set-Cookie"] instead
@response.headers['cookie'] << CGI::Cookie::new("name" => name, "value" => value)
end
return status return status
rescue MultiPartNeededException rescue MultiPartNeededException
boundary = "----------XnJLe9ZIbbGUYtzPQJ16u1" boundary = "----------XnJLe9ZIbbGUYtzPQJ16u1"
@ -305,26 +320,6 @@ module ActionController
return status return status
end end
# Parses the result of the response and extracts the various values,
# like cookies, status, headers, etc.
def parse_result
response_headers, result_body = @result.split(/\r\n\r\n/, 2)
@headers = Hash.new { |h,k| h[k] = [] }
response_headers.to_s.each_line do |line|
key, value = line.strip.split(/:\s*/, 2)
@headers[key.downcase] << value
end
(@headers['set-cookie'] || [] ).each do |string|
name, value = string.match(/^([^=]*)=([^;]*);/)[1,2]
@cookies[name] = value
end
@status, @status_message = @headers["status"].first.to_s.split(/ /)
@status = @status.to_i
end
# Encode the cookies hash in a format suitable for passing to a # Encode the cookies hash in a format suitable for passing to a
# request. # request.
def encode_cookies def encode_cookies
@ -335,13 +330,15 @@ module ActionController
# Get a temporary URL writer object # Get a temporary URL writer object
def generic_url_rewriter def generic_url_rewriter
cgi = StubCGI.new('REQUEST_METHOD' => "GET", env = {
'REQUEST_METHOD' => "GET",
'QUERY_STRING' => "", 'QUERY_STRING' => "",
"REQUEST_URI" => "/", "REQUEST_URI" => "/",
"HTTP_HOST" => host, "HTTP_HOST" => host,
"SERVER_PORT" => https? ? "443" : "80", "SERVER_PORT" => https? ? "443" : "80",
"HTTPS" => https? ? "on" : "off") "HTTPS" => https? ? "on" : "off"
ActionController::UrlRewriter.new(ActionController::CgiRequest.new(cgi), {}) }
ActionController::UrlRewriter.new(ActionController::RackRequest.new(env), {})
end end
def name_with_prefix(prefix, name) def name_with_prefix(prefix, name)
@ -442,12 +439,12 @@ EOF
end end
%w(get post put head delete cookies assigns %w(get post put head delete cookies assigns
xml_http_request get_via_redirect post_via_redirect).each do |method| xml_http_request xhr get_via_redirect post_via_redirect).each do |method|
define_method(method) do |*args| define_method(method) do |*args|
reset! unless @integration_session reset! unless @integration_session
# reset the html_document variable, but only for new get/post calls # reset the html_document variable, but only for new get/post calls
@html_document = nil unless %w(cookies assigns).include?(method) @html_document = nil unless %w(cookies assigns).include?(method)
returning @integration_session.send!(method, *args) do returning @integration_session.__send__(method, *args) do
copy_session_variables! copy_session_variables!
end end
end end
@ -472,12 +469,12 @@ EOF
self.class.fixture_table_names.each do |table_name| self.class.fixture_table_names.each do |table_name|
name = table_name.tr(".", "_") name = table_name.tr(".", "_")
next unless respond_to?(name) next unless respond_to?(name)
extras.send!(:define_method, name) { |*args| delegate.send(name, *args) } extras.__send__(:define_method, name) { |*args| delegate.send(name, *args) }
end end
end end
# delegate add_assertion to the test case # delegate add_assertion to the test case
extras.send!(:define_method, :add_assertion) { test_result.add_assertion } extras.__send__(:define_method, :add_assertion) { test_result.add_assertion }
session.extend(extras) session.extend(extras)
session.delegate = self session.delegate = self
session.test_result = @_result session.test_result = @_result
@ -491,14 +488,14 @@ EOF
def copy_session_variables! #:nodoc: def copy_session_variables! #:nodoc:
return unless @integration_session return unless @integration_session
%w(controller response request).each do |var| %w(controller response request).each do |var|
instance_variable_set("@#{var}", @integration_session.send!(var)) instance_variable_set("@#{var}", @integration_session.__send__(var))
end end
end end
# Delegate unhandled messages to the current session instance. # Delegate unhandled messages to the current session instance.
def method_missing(sym, *args, &block) def method_missing(sym, *args, &block)
reset! unless @integration_session reset! unless @integration_session
returning @integration_session.send!(sym, *args, &block) do returning @integration_session.__send__(sym, *args, &block) do
copy_session_variables! copy_session_variables!
end end
end end
@ -580,7 +577,7 @@ EOF
# end # end
# end # end
# end # end
class IntegrationTest < Test::Unit::TestCase class IntegrationTest < ActiveSupport::TestCase
include Integration::Runner include Integration::Runner
# Work around a bug in test/unit caused by the default test being named # Work around a bug in test/unit caused by the default test being named

View file

@ -3,11 +3,6 @@ module ActionController #:nodoc:
def self.included(base) def self.included(base)
base.extend(ClassMethods) base.extend(ClassMethods)
base.class_eval do base.class_eval do
# NOTE: Can't use alias_method_chain here because +render_without_layout+ is already
# defined as a publicly exposed method
alias_method :render_with_no_layout, :render
alias_method :render, :render_with_a_layout
class << self class << self
alias_method_chain :inherited, :layout alias_method_chain :inherited, :layout
end end
@ -169,17 +164,17 @@ module ActionController #:nodoc:
# performance and have access to them as any normal template would. # performance and have access to them as any normal template would.
def layout(template_name, conditions = {}, auto = false) def layout(template_name, conditions = {}, auto = false)
add_layout_conditions(conditions) add_layout_conditions(conditions)
write_inheritable_attribute "layout", template_name write_inheritable_attribute(:layout, template_name)
write_inheritable_attribute "auto_layout", auto write_inheritable_attribute(:auto_layout, auto)
end end
def layout_conditions #:nodoc: def layout_conditions #:nodoc:
@layout_conditions ||= read_inheritable_attribute("layout_conditions") @layout_conditions ||= read_inheritable_attribute(:layout_conditions)
end end
def default_layout(format) #:nodoc: def default_layout(format) #:nodoc:
layout = read_inheritable_attribute("layout") layout = read_inheritable_attribute(:layout)
return layout unless read_inheritable_attribute("auto_layout") return layout unless read_inheritable_attribute(:auto_layout)
@default_layout ||= {} @default_layout ||= {}
@default_layout[format] ||= default_layout_with_format(format, layout) @default_layout[format] ||= default_layout_with_format(format, layout)
@default_layout[format] @default_layout[format]
@ -199,7 +194,7 @@ module ActionController #:nodoc:
end end
def add_layout_conditions(conditions) def add_layout_conditions(conditions)
write_inheritable_hash "layout_conditions", normalize_conditions(conditions) write_inheritable_hash(:layout_conditions, normalize_conditions(conditions))
end end
def normalize_conditions(conditions) def normalize_conditions(conditions)
@ -221,10 +216,10 @@ module ActionController #:nodoc:
# object). If the layout was defined without a directory, layouts is assumed. So <tt>layout "weblog/standard"</tt> will return # 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. # weblog/standard, but <tt>layout "standard"</tt> will return layouts/standard.
def active_layout(passed_layout = nil) def active_layout(passed_layout = nil)
layout = passed_layout || self.class.default_layout(response.template.template_format) layout = passed_layout || self.class.default_layout(default_template_format)
active_layout = case layout active_layout = case layout
when String then layout when String then layout
when Symbol then send!(layout) when Symbol then __send__(layout)
when Proc then layout.call(self) when Proc then layout.call(self)
end end
@ -240,51 +235,24 @@ module ActionController #:nodoc:
end end
end end
protected
def render_with_a_layout(options = nil, extra_options = {}, &block) #:nodoc:
template_with_options = options.is_a?(Hash)
if (layout = pick_layout(template_with_options, options)) && apply_layout?(template_with_options, options)
options = options.merge :layout => false if template_with_options
logger.info("Rendering template within #{layout}") if logger
content_for_layout = render_with_no_layout(options, extra_options, &block)
erase_render_results
add_variables_to_assigns
@template.instance_variable_set("@content_for_layout", content_for_layout)
response.layout = layout
status = template_with_options ? options[:status] : nil
render_for_text(@template.render_file(layout, true), status)
else
render_with_no_layout(options, extra_options, &block)
end
end
private private
def apply_layout?(template_with_options, options)
return false if options == :update
template_with_options ? candidate_for_layout?(options) : !template_exempt_from_layout?
end
def candidate_for_layout?(options) def candidate_for_layout?(options)
(options.has_key?(:layout) && options[:layout] != false) || options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing, :update).compact.empty? &&
options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing).compact.empty? && !@template.__send__(:_exempt_from_layout?, options[:template] || default_template_name(options[:action]))
!template_exempt_from_layout?(options[:template] || default_template_name(options[:action]))
end end
def pick_layout(template_with_options, options) def pick_layout(options)
if template_with_options if options.has_key?(:layout)
case layout = options[:layout] case layout = options.delete(:layout)
when FalseClass when FalseClass
nil nil
when NilClass, TrueClass when NilClass, TrueClass
active_layout if action_has_layout? active_layout if action_has_layout? && !@template.__send__(:_exempt_from_layout?, default_template_name)
else else
active_layout(layout) active_layout(layout)
end end
else else
active_layout if action_has_layout? active_layout if action_has_layout? && candidate_for_layout?(options)
end end
end end
@ -304,7 +272,13 @@ module ActionController #:nodoc:
end end
def layout_directory?(layout_name) def layout_directory?(layout_name)
@template.finder.find_template_extension_from_handler(File.join('layouts', layout_name)) @template.__send__(:_pick_template, "#{File.join('layouts', layout_name)}.#{@template.template_format}") ? true : false
rescue ActionView::MissingTemplate
false
end
def default_template_format
response.template.template_format
end end
end end
end end

View file

@ -114,7 +114,11 @@ module ActionController #:nodoc:
@request = controller.request @request = controller.request
@response = controller.response @response = controller.response
if ActionController::Base.use_accept_header
@mime_type_priority = Array(Mime::Type.lookup_by_extension(@request.parameters[:format]) || @request.accepts) @mime_type_priority = Array(Mime::Type.lookup_by_extension(@request.parameters[:format]) || @request.accepts)
else
@mime_type_priority = [@request.format]
end
@order = [] @order = []
@responses = {} @responses = {}

View file

@ -1,3 +1,5 @@
require 'set'
module Mime module Mime
SET = [] SET = []
EXTENSION_LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } EXTENSION_LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? }
@ -72,6 +74,9 @@ module Mime
end end
def parse(accept_header) def parse(accept_header)
if accept_header !~ /,/
[Mime::Type.lookup(accept_header)]
else
# keep track of creation order to keep the subsequent sort stable # keep track of creation order to keep the subsequent sort stable
list = [] list = []
accept_header.split(/,/).each_with_index do |header, index| accept_header.split(/,/).each_with_index do |header, index|
@ -125,6 +130,7 @@ module Mime
list list
end end
end end
end
def initialize(string, symbol = nil, synonyms = []) def initialize(string, symbol = nil, synonyms = [])
@symbol, @synonyms = symbol, synonyms @symbol, @synonyms = symbol, synonyms

View file

@ -17,4 +17,5 @@ Mime::Type.register "multipart/form-data", :multipart_form
Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form
# http://www.ietf.org/rfc/rfc4627.txt # http://www.ietf.org/rfc/rfc4627.txt
Mime::Type.register "application/json", :json, %w( text/x-json ) # http://www.json.org/JSONRequest.html
Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )

View file

@ -0,0 +1,16 @@
require 'action_controller/integration'
require 'active_support/testing/performance'
require 'active_support/testing/default'
module ActionController
# An integration test that runs a code profiler on your test methods.
# Profiling output for combinations of each test method, measurement, and
# output format are written to your tmp/performance directory.
#
# By default, process_time is measured and both flat and graph_html output
# formats are written, so you'll have two output files per test method.
class PerformanceTest < ActionController::IntegrationTest
include ActiveSupport::Testing::Performance
include ActiveSupport::Testing::Default
end
end

View file

@ -102,7 +102,13 @@ module ActionController
args << format if format args << format if format
named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options) named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options)
send!(named_route, *args)
url_options = options.except(:action, :routing_type, :format)
unless url_options.empty?
args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options
end
__send__(named_route, *args)
end end
# Returns the path component of a URL for the given record. It uses # Returns the path component of a URL for the given record. It uses
@ -114,19 +120,19 @@ module ActionController
%w(edit new formatted).each do |action| %w(edit new formatted).each do |action|
module_eval <<-EOT, __FILE__, __LINE__ module_eval <<-EOT, __FILE__, __LINE__
def #{action}_polymorphic_url(record_or_hash) def #{action}_polymorphic_url(record_or_hash, options = {})
polymorphic_url(record_or_hash, :action => "#{action}") polymorphic_url(record_or_hash, options.merge(:action => "#{action}"))
end end
def #{action}_polymorphic_path(record_or_hash) def #{action}_polymorphic_path(record_or_hash, options = {})
polymorphic_url(record_or_hash, :action => "#{action}", :routing_type => :path) polymorphic_url(record_or_hash, options.merge(:action => "#{action}", :routing_type => :path))
end end
EOT EOT
end end
private private
def action_prefix(options) def action_prefix(options)
options[:action] ? "#{options[:action]}_" : "" options[:action] ? "#{options[:action]}_" : options[:format] ? "formatted_" : ""
end end
def routing_type(options) def routing_type(options)
@ -143,7 +149,7 @@ module ActionController
if parent.is_a?(Symbol) || parent.is_a?(String) if parent.is_a?(Symbol) || parent.is_a?(String)
string << "#{parent}_" string << "#{parent}_"
else else
string << "#{RecordIdentifier.send!("singular_class_name", parent)}_" string << "#{RecordIdentifier.__send__("singular_class_name", parent)}_"
end end
end end
end end
@ -151,7 +157,7 @@ module ActionController
if record.is_a?(Symbol) || record.is_a?(String) if record.is_a?(Symbol) || record.is_a?(String)
route << "#{record}_" route << "#{record}_"
else else
route << "#{RecordIdentifier.send!("#{inflection}_class_name", record)}_" route << "#{RecordIdentifier.__send__("#{inflection}_class_name", record)}_"
end end
action_prefix(options) + namespace + route + routing_type(options).to_s action_prefix(options) + namespace + route + routing_type(options).to_s

View file

@ -0,0 +1,303 @@
require 'action_controller/cgi_ext'
require 'action_controller/session/cookie_store'
module ActionController #:nodoc:
class RackRequest < AbstractRequest #:nodoc:
attr_accessor :session_options
attr_reader :cgi
class SessionFixationAttempt < StandardError #:nodoc:
end
DEFAULT_SESSION_OPTIONS = {
:database_manager => CGI::Session::CookieStore, # store data in cookie
:prefix => "ruby_sess.", # prefix session file names
:session_path => "/", # available to all paths in app
:session_key => "_session_id",
:cookie_only => true,
:session_http_only=> true
}
def initialize(env, session_options = DEFAULT_SESSION_OPTIONS)
@session_options = session_options
@env = env
@cgi = CGIWrapper.new(self)
super()
end
%w[ AUTH_TYPE GATEWAY_INTERFACE PATH_INFO
PATH_TRANSLATED REMOTE_HOST
REMOTE_IDENT REMOTE_USER SCRIPT_NAME
SERVER_NAME SERVER_PROTOCOL
HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env|
define_method(env.sub(/^HTTP_/n, '').downcase) do
@env[env]
end
end
def query_string
qs = super
if !qs.blank?
qs
else
@env['QUERY_STRING']
end
end
def body_stream #:nodoc:
@env['rack.input']
end
def key?(key)
@env.key?(key)
end
def cookies
return {} unless @env["HTTP_COOKIE"]
unless @env["rack.request.cookie_string"] == @env["HTTP_COOKIE"]
@env["rack.request.cookie_string"] = @env["HTTP_COOKIE"]
@env["rack.request.cookie_hash"] = CGI::Cookie::parse(@env["rack.request.cookie_string"])
end
@env["rack.request.cookie_hash"]
end
def server_port
@env['SERVER_PORT'].to_i
end
def server_software
@env['SERVER_SOFTWARE'].split("/").first
end
def session
unless defined?(@session)
if @session_options == false
@session = Hash.new
else
stale_session_check! do
if cookie_only? && query_parameters[session_options_with_string_keys['session_key']]
raise SessionFixationAttempt
end
case value = session_options_with_string_keys['new_session']
when true
@session = new_session
when false
begin
@session = CGI::Session.new(@cgi, session_options_with_string_keys)
# CGI::Session raises ArgumentError if 'new_session' == false
# and no session cookie or query param is present.
rescue ArgumentError
@session = Hash.new
end
when nil
@session = CGI::Session.new(@cgi, session_options_with_string_keys)
else
raise ArgumentError, "Invalid new_session option: #{value}"
end
@session['__valid_session']
end
end
end
@session
end
def reset_session
@session.delete if defined?(@session) && @session.is_a?(CGI::Session)
@session = new_session
end
private
# Delete an old session if it exists then create a new one.
def new_session
if @session_options == false
Hash.new
else
CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => false)).delete rescue nil
CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => true))
end
end
def cookie_only?
session_options_with_string_keys['cookie_only']
end
def stale_session_check!
yield
rescue ArgumentError => argument_error
if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
begin
# Note that the regexp does not allow $1 to end with a ':'
$1.constantize
rescue LoadError, NameError => const_error
raise ActionController::SessionRestoreError, <<-end_msg
Session contains objects whose class definition isn\'t available.
Remember to require the classes for all objects kept in the session.
(Original exception: #{const_error.message} [#{const_error.class}])
end_msg
end
retry
else
raise
end
end
def session_options_with_string_keys
@session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).stringify_keys
end
end
class RackResponse < AbstractResponse #:nodoc:
def initialize(request)
@cgi = request.cgi
@writer = lambda { |x| @body << x }
@block = nil
super()
end
# Retrieve status from instance variable if has already been delete
def status
@status || super
end
def out(output = $stdout, &block)
# Nasty hack because CGI sessions are closed after the normal
# prepare! statement
set_cookies!
@block = block
@status = headers.delete("Status")
if [204, 304].include?(status.to_i)
headers.delete("Content-Type")
[status, headers.to_hash, []]
else
[status, headers.to_hash, self]
end
end
alias to_a out
def each(&callback)
if @body.respond_to?(:call)
@writer = lambda { |x| callback.call(x) }
@body.call(self, self)
elsif @body.is_a?(String)
@body.each_line(&callback)
else
@body.each(&callback)
end
@writer = callback
@block.call(self) if @block
end
def write(str)
@writer.call str.to_s
str
end
def close
@body.close if @body.respond_to?(:close)
end
def empty?
@block == nil && @body.empty?
end
def prepare!
super
convert_language!
convert_expires!
set_status!
# set_cookies!
end
private
def convert_language!
headers["Content-Language"] = headers.delete("language") if headers["language"]
end
def convert_expires!
headers["Expires"] = headers.delete("") if headers["expires"]
end
def convert_content_type!
super
headers['Content-Type'] = headers.delete('type') || "text/html"
headers['Content-Type'] += "; charset=" + headers.delete('charset') if headers['charset']
end
def set_content_length!
super
headers["Content-Length"] = headers["Content-Length"].to_s if headers["Content-Length"]
end
def set_status!
self.status ||= "200 OK"
end
def set_cookies!
# Convert 'cookie' header to 'Set-Cookie' headers.
# Because Set-Cookie header can appear more the once in the response body,
# we store it in a line break separated string that will be translated to
# multiple Set-Cookie header by the handler.
if cookie = headers.delete('cookie')
cookies = []
case cookie
when Array then cookie.each { |c| cookies << c.to_s }
when Hash then cookie.each { |_, c| cookies << c.to_s }
else cookies << cookie.to_s
end
@cgi.output_cookies.each { |c| cookies << c.to_s } if @cgi.output_cookies
headers['Set-Cookie'] = [headers['Set-Cookie'], cookies].flatten.compact
end
end
end
class CGIWrapper < ::CGI
attr_reader :output_cookies
def initialize(request, *args)
@request = request
@args = *args
@input = request.body
super *args
end
def params
@params ||= @request.params
end
def cookies
@request.cookies
end
def query_string
@request.query_string
end
# Used to wrap the normal args variable used inside CGI.
def args
@args
end
# Used to wrap the normal env_table variable used inside CGI.
def env_table
@request.env
end
# Used to wrap the normal stdinput variable used inside CGI.
def stdinput
@input
end
end
end

View file

@ -2,33 +2,37 @@ require 'tempfile'
require 'stringio' require 'stringio'
require 'strscan' require 'strscan'
module ActionController require 'active_support/memoizable'
# HTTP methods which are accepted by default.
ACCEPTED_HTTP_METHODS = Set.new(%w( get head put post delete options ))
module ActionController
# CgiRequest and TestRequest provide concrete implementations. # CgiRequest and TestRequest provide concrete implementations.
class AbstractRequest class AbstractRequest
cattr_accessor :relative_url_root extend ActiveSupport::Memoizable
remove_method :relative_url_root
def self.relative_url_root=(*args)
ActiveSupport::Deprecation.warn(
"ActionController::AbstractRequest.relative_url_root= has been renamed." +
"You can now set it with config.action_controller.relative_url_root=", caller)
end
HTTP_METHODS = %w(get head put post delete options)
HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h }
# The hash of environment variables for this request, # The hash of environment variables for this request,
# such as { 'RAILS_ENV' => 'production' }. # such as { 'RAILS_ENV' => 'production' }.
attr_reader :env attr_reader :env
# The true HTTP request method as a lowercase symbol, such as <tt>:get</tt>. # The true HTTP request \method as a lowercase symbol, such as <tt>:get</tt>.
# UnknownHttpMethod is raised for invalid methods not listed in ACCEPTED_HTTP_METHODS. # UnknownHttpMethod is raised for invalid methods not listed in ACCEPTED_HTTP_METHODS.
def request_method def request_method
@request_method ||= begin method = @env['REQUEST_METHOD']
method = ((@env['REQUEST_METHOD'] == 'POST' && !parameters[:_method].blank?) ? parameters[:_method].to_s : @env['REQUEST_METHOD']).downcase method = parameters[:_method] if method == 'POST' && !parameters[:_method].blank?
if ACCEPTED_HTTP_METHODS.include?(method)
method.to_sym
else
raise UnknownHttpMethod, "#{method}, accepted HTTP methods are #{ACCEPTED_HTTP_METHODS.to_a.to_sentence}"
end
end
end
# The HTTP request method as a lowercase symbol, such as <tt>:get</tt>. HTTP_METHOD_LOOKUP[method] || raise(UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}")
end
memoize :request_method
# The HTTP request \method as a lowercase symbol, such as <tt>:get</tt>.
# Note, HEAD is returned as <tt>:get</tt> since the two are functionally # Note, HEAD is returned as <tt>:get</tt> since the two are functionally
# equivalent from the application's perspective. # equivalent from the application's perspective.
def method def method
@ -55,53 +59,103 @@ module ActionController
request_method == :delete request_method == :delete
end end
# Is this a HEAD request? <tt>request.method</tt> sees HEAD as <tt>:get</tt>, # Is this a HEAD request? Since <tt>request.method</tt> sees HEAD as <tt>:get</tt>,
# so check the HTTP method directly. # this \method checks the actual HTTP \method directly.
def head? def head?
request_method == :head request_method == :head
end end
# Provides acccess to the request's HTTP headers, for example: # Provides access to the request's HTTP headers, for example:
#
# request.headers["Content-Type"] # => "text/plain" # request.headers["Content-Type"] # => "text/plain"
def headers def headers
@headers ||= ActionController::Http::Headers.new(@env) ActionController::Http::Headers.new(@env)
end end
memoize :headers
# Returns the content length of the request as an integer.
def content_length def content_length
@content_length ||= env['CONTENT_LENGTH'].to_i @env['CONTENT_LENGTH'].to_i
end end
memoize :content_length
# The MIME type of the HTTP request, such as Mime::XML. # The MIME type of the HTTP request, such as Mime::XML.
# #
# For backward compatibility, the post format is extracted from the # For backward compatibility, the post \format is extracted from the
# X-Post-Data-Format HTTP header if present. # X-Post-Data-Format HTTP header if present.
def content_type def content_type
@content_type ||= Mime::Type.lookup(content_type_without_parameters) Mime::Type.lookup(content_type_without_parameters)
end end
memoize :content_type
# Returns the accepted MIME type for the request # Returns the accepted MIME type for the request.
def accepts def accepts
@accepts ||= header = @env['HTTP_ACCEPT'].to_s.strip
if @env['HTTP_ACCEPT'].to_s.strip.empty?
[ content_type, Mime::ALL ].compact # make sure content_type being nil is not included if header.empty?
[content_type, Mime::ALL].compact
else else
Mime::Type.parse(@env['HTTP_ACCEPT']) Mime::Type.parse(header)
end
end
memoize :accepts
def if_modified_since
if since = env['HTTP_IF_MODIFIED_SINCE']
Time.rfc2822(since) rescue nil
end
end
memoize :if_modified_since
def if_none_match
env['HTTP_IF_NONE_MATCH']
end
def not_modified?(modified_at)
if_modified_since && modified_at && if_modified_since >= modified_at
end
def etag_matches?(etag)
if_none_match && if_none_match == etag
end
# Check response freshness (Last-Modified and ETag) against request
# If-Modified-Since and If-None-Match conditions. If both headers are
# supplied, both must match, or the request is not considered fresh.
def fresh?(response)
case
when if_modified_since && if_none_match
not_modified?(response.last_modified) && etag_matches?(response.etag)
when if_modified_since
not_modified?(response.last_modified)
when if_none_match
etag_matches?(response.etag)
else
false
end end
end end
# Returns the Mime type for the format used in the request. If there is no format available, the first of the # Returns the Mime type for the \format used in the request.
# accept types will be used. Examples:
# #
# GET /posts/5.xml | request.format => Mime::XML # GET /posts/5.xml | request.format => Mime::XML
# GET /posts/5.xhtml | request.format => Mime::HTML # GET /posts/5.xhtml | request.format => Mime::HTML
# GET /posts/5 | request.format => request.accepts.first (usually Mime::HTML for browsers) # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of <tt>ActionController::Base.use_accept_header</tt>
def format def format
@format ||= parameters[:format] ? Mime::Type.lookup_by_extension(parameters[:format]) : accepts.first @format ||=
if parameters[:format]
Mime::Type.lookup_by_extension(parameters[:format])
elsif ActionController::Base.use_accept_header
accepts.first
elsif xhr?
Mime::Type.lookup_by_extension("js")
else
Mime::Type.lookup_by_extension("html")
end
end end
# Sets the format by string extension, which can be used to force custom formats that are not controlled by the extension. # Sets the \format by string extension, which can be used to force custom formats
# Example: # that are not controlled by the extension.
# #
# class ApplicationController < ActionController::Base # class ApplicationController < ActionController::Base
# before_filter :adjust_format_for_iphone # before_filter :adjust_format_for_iphone
@ -116,6 +170,25 @@ module ActionController
@format = Mime::Type.lookup_by_extension(parameters[:format]) @format = Mime::Type.lookup_by_extension(parameters[:format])
end end
# Returns a symbolized version of the <tt>:format</tt> parameter of the request.
# If no \format is given it returns <tt>:js</tt>for Ajax requests and <tt>:html</tt>
# otherwise.
def template_format
parameter_format = parameters[:format]
if parameter_format
parameter_format
elsif xhr?
:js
else
:html
end
end
def cache_format
parameters[:format]
end
# Returns true if the request's "X-Requested-With" header contains # Returns true if the request's "X-Requested-With" header contains
# "XMLHttpRequest". (The Prototype Javascript library sends this header with # "XMLHttpRequest". (The Prototype Javascript library sends this header with
# every Ajax request.) # every Ajax request.)
@ -128,7 +201,7 @@ module ActionController
# the right-hand-side of X-Forwarded-For # the right-hand-side of X-Forwarded-For
TRUSTED_PROXIES = /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i TRUSTED_PROXIES = /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i
# Determine originating IP address. REMOTE_ADDR is the standard # Determines originating IP address. REMOTE_ADDR is the standard
# but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or # 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 if # HTTP_X_FORWARDED_FOR are set by proxies so check for these if
# REMOTE_ADDR is a proxy. HTTP_X_FORWARDED_FOR may be a comma- # REMOTE_ADDR is a proxy. HTTP_X_FORWARDED_FOR may be a comma-
@ -166,44 +239,65 @@ EOM
@env['REMOTE_ADDR'] @env['REMOTE_ADDR']
end end
memoize :remote_ip
# Returns the lowercase name of the HTTP server software. # Returns the lowercase name of the HTTP server software.
def server_software def server_software
(@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
end end
memoize :server_software
# Returns the complete URL used for this request # Returns the complete URL used for this request.
def url def url
protocol + host_with_port + request_uri protocol + host_with_port + request_uri
end end
memoize :url
# Return 'https://' if this is an SSL request and 'http://' otherwise. # Returns 'https://' if this is an SSL request and 'http://' otherwise.
def protocol def protocol
ssl? ? 'https://' : 'http://' ssl? ? 'https://' : 'http://'
end end
memoize :protocol
# Is this an SSL request? # Is this an SSL request?
def ssl? def ssl?
@env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https' @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
end end
# Returns the host for this request, such as example.com. # Returns the \host for this request, such as "example.com".
def host def raw_host_with_port
if forwarded = env["HTTP_X_FORWARDED_HOST"]
forwarded.split(/,\s?/).last
else
env['HTTP_HOST'] || env['SERVER_NAME'] || "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
end
end end
# Returns a host:port string for this request, such as example.com or # Returns the host for this request, such as example.com.
# example.com:8080. def host
def host_with_port raw_host_with_port.sub(/:\d+$/, '')
@host_with_port ||= host + port_string
end end
memoize :host
# Returns a \host:\port string for this request, such as "example.com" or
# "example.com:8080".
def host_with_port
"#{host}#{port_string}"
end
memoize :host_with_port
# Returns the port number of this request as an integer. # Returns the port number of this request as an integer.
def port def port
@port_as_int ||= @env['SERVER_PORT'].to_i if raw_host_with_port =~ /:(\d+)$/
$1.to_i
else
standard_port
end end
end
memoize :port
# Returns the standard port number for this request's protocol # Returns the standard \port number for this request's protocol.
def standard_port def standard_port
case protocol case protocol
when 'https://' then 443 when 'https://' then 443
@ -211,13 +305,13 @@ EOM
end end
end end
# Returns a port suffix like ":8080" if the port number of this request # Returns a \port suffix like ":8080" if the \port number of this request
# is not the default HTTP port 80 or HTTPS port 443. # is not the default HTTP \port 80 or HTTPS \port 443.
def port_string def port_string
(port == standard_port) ? '' : ":#{port}" port == standard_port ? '' : ":#{port}"
end end
# Returns the domain part of a host, such as rubyonrails.org in "www.rubyonrails.org". You can specify # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
# a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk". # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
def domain(tld_length = 1) def domain(tld_length = 1)
return nil unless named_host?(host) return nil unless named_host?(host)
@ -225,8 +319,9 @@ EOM
host.split('.').last(1 + tld_length).join('.') host.split('.').last(1 + tld_length).join('.')
end end
# Returns all the subdomains as an array, so ["dev", "www"] would be returned for "dev.www.rubyonrails.org". # Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be
# You can specify a different <tt>tld_length</tt>, such as 2 to catch ["www"] instead of ["www", "rubyonrails"] # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
# such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt>
# in "www.rubyonrails.co.uk". # in "www.rubyonrails.co.uk".
def subdomains(tld_length = 1) def subdomains(tld_length = 1)
return [] unless named_host?(host) return [] unless named_host?(host)
@ -234,7 +329,7 @@ EOM
parts[0..-(tld_length+2)] parts[0..-(tld_length+2)]
end end
# Return the query string, accounting for server idiosyncracies. # Returns the query string, accounting for server idiosyncrasies.
def query_string def query_string
if uri = @env['REQUEST_URI'] if uri = @env['REQUEST_URI']
uri.split('?', 2)[1] || '' uri.split('?', 2)[1] || ''
@ -242,8 +337,9 @@ EOM
@env['QUERY_STRING'] || '' @env['QUERY_STRING'] || ''
end end
end end
memoize :query_string
# Return the request URI, accounting for server idiosyncracies. # Returns the request URI, accounting for server idiosyncrasies.
# WEBrick includes the full URL. IIS leaves REQUEST_URI blank. # WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
def request_uri def request_uri
if uri = @env['REQUEST_URI'] if uri = @env['REQUEST_URI']
@ -251,48 +347,36 @@ EOM
(%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri
else else
# Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO. # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO.
script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$}) uri = @env['PATH_INFO'].to_s
uri = @env['PATH_INFO']
uri = uri.sub(/#{script_filename}\//, '') unless script_filename.nil? if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
unless (env_qs = @env['QUERY_STRING']).nil? || env_qs.empty? uri = uri.sub(/#{script_filename}\//, '')
uri << '?' << env_qs
end end
if uri.nil? env_qs = @env['QUERY_STRING'].to_s
uri += "?#{env_qs}" unless env_qs.empty?
if uri.blank?
@env.delete('REQUEST_URI') @env.delete('REQUEST_URI')
uri
else else
@env['REQUEST_URI'] = uri @env['REQUEST_URI'] = uri
end end
end end
end end
memoize :request_uri
# Returns the interpreted path to requested resource after all the installation directory of this application was taken into account # Returns the interpreted \path to requested resource after all the installation
# directory of this application was taken into account.
def path def path
path = (uri = request_uri) ? uri.split('?').first.to_s : '' path = (uri = request_uri) ? uri.split('?').first.to_s : ''
# Cut off the path to the installation directory if given # Cut off the path to the installation directory if given
path.sub!(%r/^#{relative_url_root}/, '') path.sub!(%r/^#{ActionController::Base.relative_url_root}/, '')
path || '' path || ''
end end
memoize :path
# Returns the path minus the web server relative installation directory. # Read the request \body. This is useful for web services that need to
# This can be set with the environment variable RAILS_RELATIVE_URL_ROOT.
# It can be automatically extracted for Apache setups. If the server is not
# Apache, this method returns an empty string.
def relative_url_root
@@relative_url_root ||= case
when @env["RAILS_RELATIVE_URL_ROOT"]
@env["RAILS_RELATIVE_URL_ROOT"]
when server_software == 'apache'
@env["SCRIPT_NAME"].to_s.sub(/\/dispatch\.(fcgi|rb|cgi)$/, '')
else
''
end
end
# Read the request body. This is useful for web services that need to
# work with raw requests directly. # work with raw requests directly.
def raw_post def raw_post
unless env.include? 'RAW_POST_DATA' unless env.include? 'RAW_POST_DATA'
@ -302,7 +386,7 @@ EOM
env['RAW_POST_DATA'] env['RAW_POST_DATA']
end end
# Returns both GET and POST parameters in a single hash. # Returns both GET and POST \parameters in a single hash.
def parameters def parameters
@parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access @parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access
end end
@ -312,34 +396,56 @@ EOM
@symbolized_path_parameters = @parameters = nil @symbolized_path_parameters = @parameters = nil
end end
# The same as <tt>path_parameters</tt> with explicitly symbolized keys # The same as <tt>path_parameters</tt> with explicitly symbolized keys.
def symbolized_path_parameters def symbolized_path_parameters
@symbolized_path_parameters ||= path_parameters.symbolize_keys @symbolized_path_parameters ||= path_parameters.symbolize_keys
end end
# Returns a hash with the parameters used to form the path of the request. # Returns a hash with the \parameters used to form the \path of the request.
# Returned hash keys are strings. See <tt>symbolized_path_parameters</tt> for symbolized keys. # Returned hash keys are strings:
#
# Example:
# #
# {'action' => 'my_action', 'controller' => 'my_controller'} # {'action' => 'my_action', 'controller' => 'my_controller'}
#
# See <tt>symbolized_path_parameters</tt> for symbolized keys.
def path_parameters def path_parameters
@path_parameters ||= {} @path_parameters ||= {}
end end
# The request body is an IO input stream. If the RAW_POST_DATA environment
# variable is already set, wrap it in a StringIO.
def body
if raw_post = env['RAW_POST_DATA']
raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding)
StringIO.new(raw_post)
else
body_stream
end
end
def remote_addr
@env['REMOTE_ADDR']
end
def referrer
@env['HTTP_REFERER']
end
alias referer referrer
def query_parameters
@query_parameters ||= self.class.parse_query_parameters(query_string)
end
def request_parameters
@request_parameters ||= parse_formatted_request_parameters
end
#-- #--
# Must be implemented in the concrete request # Must be implemented in the concrete request
#++ #++
# The request body is an IO input stream. def body_stream #:nodoc:
def body
end
def query_parameters #:nodoc:
end
def request_parameters #:nodoc:
end end
def cookies #:nodoc: def cookies #:nodoc:
@ -366,8 +472,9 @@ EOM
# The raw content type string with its parameters stripped off. # The raw content type string with its parameters stripped off.
def content_type_without_parameters def content_type_without_parameters
@content_type_without_parameters ||= self.class.extract_content_type_without_parameters(content_type_with_parameters) self.class.extract_content_type_without_parameters(content_type_with_parameters)
end end
memoize :content_type_without_parameters
private private
def content_type_from_legacy_post_data_format_header def content_type_from_legacy_post_data_format_header

View file

@ -17,7 +17,7 @@ module ActionController #:nodoc:
# forged link from another site, is done by embedding a token based on the session (which an attacker wouldn't know) in all # forged link from another site, is done by embedding a token based on the session (which an attacker wouldn't know) in all
# forms and Ajax requests generated by Rails and then verifying the authenticity of that token in the controller. Only # forms and Ajax requests generated by Rails and then verifying the authenticity of that token in the controller. Only
# HTML/JavaScript requests are checked, so this will not protect your XML API (presumably you'll have a different authentication # HTML/JavaScript requests are checked, so this will not protect your XML API (presumably you'll have a different authentication
# scheme there anyway). Also, GET requests are not protected as these should be indempotent anyway. # scheme there anyway). Also, GET requests are not protected as these should be idempotent anyway.
# #
# This is turned on with the <tt>protect_from_forgery</tt> method, which will check the token and raise an # This is turned on with the <tt>protect_from_forgery</tt> method, which will check the token and raise an
# ActionController::InvalidAuthenticityToken if it doesn't match what was expected. You can customize the error message in # ActionController::InvalidAuthenticityToken if it doesn't match what was expected. You can customize the error message in

View file

@ -41,10 +41,9 @@ module ActionController #:nodoc:
base.rescue_templates = Hash.new(DEFAULT_RESCUE_TEMPLATE) base.rescue_templates = Hash.new(DEFAULT_RESCUE_TEMPLATE)
base.rescue_templates.update DEFAULT_RESCUE_TEMPLATES base.rescue_templates.update DEFAULT_RESCUE_TEMPLATES
base.class_inheritable_array :rescue_handlers
base.rescue_handlers = []
base.extend(ClassMethods) base.extend(ClassMethods)
base.send :include, ActiveSupport::Rescuable
base.class_eval do base.class_eval do
alias_method_chain :perform_action, :rescue alias_method_chain :perform_action, :rescue
end end
@ -54,78 +53,12 @@ module ActionController #:nodoc:
def process_with_exception(request, response, exception) #:nodoc: def process_with_exception(request, response, exception) #:nodoc:
new.process(request, response, :rescue_action, exception) new.process(request, response, :rescue_action, exception)
end end
# Rescue exceptions raised in controller actions.
#
# <tt>rescue_from</tt> receives a series of exception classes or class
# names, and a trailing <tt>:with</tt> option with the name of a method
# or a Proc object to be called to handle them. Alternatively a block can
# be given.
#
# Handlers that take one argument will be called with the exception, so
# that the exception can be inspected when dealing with it.
#
# Handlers are inherited. They are searched from right to left, from
# bottom to top, and up the hierarchy. The handler of the first class for
# which <tt>exception.is_a?(klass)</tt> holds true is the one invoked, if
# any.
#
# class ApplicationController < ActionController::Base
# rescue_from User::NotAuthorized, :with => :deny_access # self defined exception
# rescue_from ActiveRecord::RecordInvalid, :with => :show_errors
#
# rescue_from 'MyAppError::Base' do |exception|
# render :xml => exception, :status => 500
# end
#
# protected
# def deny_access
# ...
# end
#
# def show_errors(exception)
# exception.record.new_record? ? ...
# end
# end
def rescue_from(*klasses, &block)
options = klasses.extract_options!
unless options.has_key?(:with)
block_given? ? options[:with] = block : raise(ArgumentError, "Need a handler. Supply an options hash that has a :with key as the last argument.")
end
klasses.each do |klass|
key = if klass.is_a?(Class) && klass <= Exception
klass.name
elsif klass.is_a?(String)
klass
else
raise(ArgumentError, "#{klass} is neither an Exception nor a String")
end
# Order is important, we put the pair at the end. When dealing with an
# exception we will follow the documented order going from right to left.
rescue_handlers << [key, options[:with]]
end
end
end end
protected protected
# Exception handler called when the performance of an action raises an exception. # Exception handler called when the performance of an action raises an exception.
def rescue_action(exception) def rescue_action(exception)
log_error(exception) if logger rescue_with_handler(exception) || rescue_action_without_handler(exception)
erase_results if performed?
# Let the exception alter the response if it wants.
# For example, MethodNotAllowed sets the Allow header.
if exception.respond_to?(:handle_response!)
exception.handle_response!(response)
end
if consider_all_requests_local || local_request?
rescue_action_locally(exception)
else
rescue_action_in_public(exception)
end
end end
# Overwrite to implement custom logging of errors. By default logs as fatal. # Overwrite to implement custom logging of errors. By default logs as fatal.
@ -144,7 +77,7 @@ module ActionController #:nodoc:
end end
# Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>). By # Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>). By
# default will call render_optional_error_file. Override this method to provide more user friendly error messages.s # default will call render_optional_error_file. Override this method to provide more user friendly error messages.
def rescue_action_in_public(exception) #:doc: def rescue_action_in_public(exception) #:doc:
render_optional_error_file response_code_for_rescue(exception) render_optional_error_file response_code_for_rescue(exception)
end end
@ -173,26 +106,28 @@ module ActionController #:nodoc:
# Render detailed diagnostics for unhandled exceptions rescued from # Render detailed diagnostics for unhandled exceptions rescued from
# a controller action. # a controller action.
def rescue_action_locally(exception) def rescue_action_locally(exception)
add_variables_to_assigns
@template.instance_variable_set("@exception", exception) @template.instance_variable_set("@exception", exception)
@template.instance_variable_set("@rescues_path", File.dirname(rescues_path("stub"))) @template.instance_variable_set("@rescues_path", File.dirname(rescues_path("stub")))
@template.send!(:assign_variables_from_controller) @template.instance_variable_set("@contents", @template.render(:file => template_path_for_local_rescue(exception)))
@template.instance_variable_set("@contents", @template.render_file(template_path_for_local_rescue(exception), false))
response.content_type = Mime::HTML response.content_type = Mime::HTML
render_for_file(rescues_path("layout"), response_code_for_rescue(exception)) render_for_file(rescues_path("layout"), response_code_for_rescue(exception))
end end
# Tries to rescue the exception by looking up and calling a registered handler. def rescue_action_without_handler(exception)
def rescue_action_with_handler(exception) log_error(exception) if logger
if handler = handler_for_rescue(exception) erase_results if performed?
if handler.arity != 0
handler.call(exception) # Let the exception alter the response if it wants.
else # For example, MethodNotAllowed sets the Allow header.
handler.call if exception.respond_to?(:handle_response!)
exception.handle_response!(response)
end end
true # don't rely on the return value of the handler
if consider_all_requests_local || local_request?
rescue_action_locally(exception)
else
rescue_action_in_public(exception)
end end
end end
@ -200,7 +135,7 @@ module ActionController #:nodoc:
def perform_action_with_rescue #:nodoc: def perform_action_with_rescue #:nodoc:
perform_action_without_rescue perform_action_without_rescue
rescue Exception => exception rescue Exception => exception
rescue_action_with_handler(exception) || rescue_action(exception) rescue_action(exception)
end end
def rescues_path(template_name) def rescues_path(template_name)
@ -215,36 +150,6 @@ module ActionController #:nodoc:
rescue_responses[exception.class.name] rescue_responses[exception.class.name]
end end
def handler_for_rescue(exception)
# We go from right to left because pairs are pushed onto rescue_handlers
# as rescue_from declarations are found.
_, handler = *rescue_handlers.reverse.detect do |klass_name, handler|
# The purpose of allowing strings in rescue_from is to support the
# declaration of handler associations for exception classes whose
# definition is yet unknown.
#
# Since this loop needs the constants it would be inconsistent to
# assume they should exist at this point. An early raised exception
# could trigger some other handler and the array could include
# precisely a string whose corresponding constant has not yet been
# seen. This is why we are tolerant to unknown constants.
#
# Note that this tolerance only matters if the exception was given as
# a string, otherwise a NameError will be raised by the interpreter
# itself when rescue_from CONSTANT is executed.
klass = self.class.const_get(klass_name) rescue nil
klass ||= klass_name.constantize rescue nil
exception.is_a?(klass) if klass
end
case handler
when Symbol
method(handler)
when Proc
handler.bind(self)
end
end
def clean_backtrace(exception) def clean_backtrace(exception)
if backtrace = exception.backtrace if backtrace = exception.backtrace
if defined?(RAILS_ROOT) if defined?(RAILS_ROOT)

View file

@ -1,23 +1,23 @@
module ActionController module ActionController
# == Overview # == Overview
# #
# ActionController::Resources are a way of defining RESTful resources. A RESTful resource, in basic terms, # ActionController::Resources are a way of defining RESTful \resources. A RESTful \resource, in basic terms,
# is something that can be pointed at and it will respond with a representation of the data requested. # is something that can be pointed at and it will respond with a representation of the data requested.
# In real terms this could mean a user with a browser requests an HTML page, or that a desktop application # In real terms this could mean a user with a browser requests an HTML page, or that a desktop application
# requests XML data. # requests XML data.
# #
# RESTful design is based on the assumption that there are four generic verbs that a user of an # RESTful design is based on the assumption that there are four generic verbs that a user of an
# application can request from a resource (the noun). # application can request from a \resource (the noun).
# #
# Resources can be requested using four basic HTTP verbs (GET, POST, PUT, DELETE), the method used # \Resources can be requested using four basic HTTP verbs (GET, POST, PUT, DELETE), the method used
# denotes the type of action that should take place. # denotes the type of action that should take place.
# #
# === The Different Methods and their Usage # === The Different Methods and their Usage
# #
# +GET+ Requests for a resource, no saving or editing of a resource should occur in a GET request # * GET - Requests for a \resource, no saving or editing of a \resource should occur in a GET request.
# +POST+ Creation of resources # * POST - Creation of \resources.
# +PUT+ Editing of attributes on a resource # * PUT - Editing of attributes on a \resource.
# +DELETE+ Deletion of a resource # * DELETE - Deletion of a \resource.
# #
# === Examples # === Examples
# #
@ -72,7 +72,7 @@ module ActionController
end end
def conditions def conditions
@conditions = @options[:conditions] || {} @conditions ||= @options[:conditions] || {}
end end
def path def path
@ -85,16 +85,24 @@ module ActionController
@new_path ||= "#{path}/#{new_action}" @new_path ||= "#{path}/#{new_action}"
end end
def shallow_path_prefix
@shallow_path_prefix ||= "#{path_prefix unless @options[:shallow]}"
end
def member_path def member_path
@member_path ||= "#{path}/:id" @member_path ||= "#{shallow_path_prefix}/#{path_segment}/:id"
end end
def nesting_path_prefix def nesting_path_prefix
@nesting_path_prefix ||= "#{path}/:#{singular}_id" @nesting_path_prefix ||= "#{shallow_path_prefix}/#{path_segment}/:#{singular}_id"
end
def shallow_name_prefix
@shallow_name_prefix ||= "#{name_prefix unless @options[:shallow]}"
end end
def nesting_name_prefix def nesting_name_prefix
"#{name_prefix}#{singular}_" "#{shallow_name_prefix}#{singular}_"
end end
def action_separator def action_separator
@ -141,12 +149,14 @@ module ActionController
super super
end end
alias_method :shallow_path_prefix, :path_prefix
alias_method :shallow_name_prefix, :name_prefix
alias_method :member_path, :path alias_method :member_path, :path
alias_method :nesting_path_prefix, :path alias_method :nesting_path_prefix, :path
end end
# Creates named routes for implementing verb-oriented controllers # Creates named routes for implementing verb-oriented controllers
# for a collection resource. # for a collection \resource.
# #
# For example: # For example:
# #
@ -238,23 +248,24 @@ module ActionController
# #
# The +resources+ method accepts the following options to customize the resulting routes: # The +resources+ method accepts the following options to customize the resulting routes:
# * <tt>:collection</tt> - Add named routes for other actions that operate on the collection. # * <tt>:collection</tt> - Add named routes for other actions that operate on the collection.
# Takes a hash of <tt>#{action} => #{method}</tt>, where method is <tt>:get</tt>/<tt>:post</tt>/<tt>:put</tt>/<tt>:delete</tt> # Takes a hash of <tt>#{action} => #{method}</tt>, where method is <tt>:get</tt>/<tt>:post</tt>/<tt>:put</tt>/<tt>:delete</tt>,
# or <tt>:any</tt> if the method does not matter. These routes map to a URL like /messages/rss, with a route of +rss_messages_url+. # an array of any of the previous, or <tt>:any</tt> if the method does not matter.
# These routes map to a URL like /messages/rss, with a route of +rss_messages_url+.
# * <tt>:member</tt> - Same as <tt>:collection</tt>, but for actions that operate on a specific member. # * <tt>:member</tt> - Same as <tt>:collection</tt>, but for actions that operate on a specific member.
# * <tt>:new</tt> - Same as <tt>:collection</tt>, but for actions that operate on the new resource action. # * <tt>:new</tt> - Same as <tt>:collection</tt>, but for actions that operate on the new \resource action.
# * <tt>:controller</tt> - Specify the controller name for the routes. # * <tt>:controller</tt> - Specify the controller name for the routes.
# * <tt>:singular</tt> - Specify the singular name used in the member routes. # * <tt>:singular</tt> - Specify the singular name used in the member routes.
# * <tt>:requirements</tt> - Set custom routing parameter requirements. # * <tt>:requirements</tt> - Set custom routing parameter requirements.
# * <tt>:conditions</tt> - Specify custom routing recognition conditions. Resources sets the <tt>:method</tt> value for the method-specific routes. # * <tt>:conditions</tt> - Specify custom routing recognition conditions. \Resources sets the <tt>:method</tt> value for the method-specific routes.
# * <tt>:as</tt> - Specify a different resource name to use in the URL path. For example: # * <tt>:as</tt> - Specify a different \resource name to use in the URL path. For example:
# # products_path == '/productos' # # products_path == '/productos'
# map.resources :products, :as => 'productos' do |product| # map.resources :products, :as => 'productos' do |product|
# # product_reviews_path(product) == '/productos/1234/comentarios' # # product_reviews_path(product) == '/productos/1234/comentarios'
# product.resources :product_reviews, :as => 'comentarios' # product.resources :product_reviews, :as => 'comentarios'
# end # end
# #
# * <tt>:has_one</tt> - Specify nested resources, this is a shorthand for mapping singleton resources beneath the current. # * <tt>:has_one</tt> - Specify nested \resources, this is a shorthand for mapping singleton \resources beneath the current.
# * <tt>:has_many</tt> - Same has <tt>:has_one</tt>, but for plural resources. # * <tt>:has_many</tt> - Same has <tt>:has_one</tt>, but for plural \resources.
# #
# You may directly specify the routing association with +has_one+ and +has_many+ like: # You may directly specify the routing association with +has_one+ and +has_many+ like:
# #
@ -277,18 +288,18 @@ module ActionController
# #
# * <tt>:path_prefix</tt> - Set a prefix to the routes with required route variables. # * <tt>:path_prefix</tt> - Set a prefix to the routes with required route variables.
# #
# Weblog comments usually belong to a post, so you might use resources like: # Weblog comments usually belong to a post, so you might use +resources+ like:
# #
# map.resources :articles # map.resources :articles
# map.resources :comments, :path_prefix => '/articles/:article_id' # map.resources :comments, :path_prefix => '/articles/:article_id'
# #
# You can nest resources calls to set this automatically: # You can nest +resources+ calls to set this automatically:
# #
# map.resources :articles do |article| # map.resources :articles do |article|
# article.resources :comments # article.resources :comments
# end # end
# #
# The comment resources work the same, but must now include a value for <tt>:article_id</tt>. # The comment \resources work the same, but must now include a value for <tt>:article_id</tt>.
# #
# article_comments_url(@article) # article_comments_url(@article)
# article_comment_url(@article, @comment) # article_comment_url(@article, @comment)
@ -296,23 +307,52 @@ module ActionController
# article_comments_url(:article_id => @article) # article_comments_url(:article_id => @article)
# article_comment_url(:article_id => @article, :id => @comment) # article_comment_url(:article_id => @article, :id => @comment)
# #
# If you don't want to load all objects from the database you might want to use the <tt>article_id</tt> directly:
#
# articles_comments_url(@comment.article_id, @comment)
#
# * <tt>:name_prefix</tt> - Define a prefix for all generated routes, usually ending in an underscore. # * <tt>:name_prefix</tt> - Define a prefix for all generated routes, usually ending in an underscore.
# Use this if you have named routes that may clash. # Use this if you have named routes that may clash.
# #
# map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_' # map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_'
# map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_' # map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_'
# #
# You may also use <tt>:name_prefix</tt> to override the generic named routes in a nested resource: # You may also use <tt>:name_prefix</tt> to override the generic named routes in a nested \resource:
# #
# map.resources :articles do |article| # map.resources :articles do |article|
# article.resources :comments, :name_prefix => nil # article.resources :comments, :name_prefix => nil
# end # end
# #
# This will yield named resources like so: # This will yield named \resources like so:
# #
# comments_url(@article) # comments_url(@article)
# comment_url(@article, @comment) # comment_url(@article, @comment)
# #
# * <tt>:shallow</tt> - If true, paths for nested resources which reference a specific member
# (ie. those with an :id parameter) will not use the parent path prefix or name prefix.
#
# The <tt>:shallow</tt> option is inherited by any nested resource(s).
#
# For example, 'users', 'posts' and 'comments' all use shallow paths with the following nested resources:
#
# map.resources :users, :shallow => true do |user|
# user.resources :posts do |post|
# post.resources :comments
# end
# end
# # --> GET /users/1/posts (maps to the PostsController#index action as usual)
# # also adds the usual named route called "user_posts"
# # --> GET /posts/2 (maps to the PostsController#show action as if it were not nested)
# # also adds the named route called "post"
# # --> GET /posts/2/comments (maps to the CommentsController#index action)
# # also adds the named route called "post_comments"
# # --> GET /comments/2 (maps to the CommentsController#show action as if it were not nested)
# # also adds the named route called "comment"
#
# You may also use <tt>:shallow</tt> in combination with the +has_one+ and +has_many+ shorthand notations like:
#
# map.resources :users, :has_many => { :posts => :comments }, :shallow => true
#
# If <tt>map.resources</tt> is called with multiple resources, they all get the same options applied. # If <tt>map.resources</tt> is called with multiple resources, they all get the same options applied.
# #
# Examples: # Examples:
@ -345,28 +385,28 @@ module ActionController
# #
# The +resources+ method sets HTTP method restrictions on the routes it generates. For example, making an # The +resources+ method sets HTTP method restrictions on the routes it generates. For example, making an
# HTTP POST on <tt>new_message_url</tt> will raise a RoutingError exception. The default route in # HTTP POST on <tt>new_message_url</tt> will raise a RoutingError exception. The default route in
# <tt>config/routes.rb</tt> overrides this and allows invalid HTTP methods for resource routes. # <tt>config/routes.rb</tt> overrides this and allows invalid HTTP methods for \resource routes.
def resources(*entities, &block) def resources(*entities, &block)
options = entities.extract_options! options = entities.extract_options!
entities.each { |entity| map_resource(entity, options.dup, &block) } entities.each { |entity| map_resource(entity, options.dup, &block) }
end end
# Creates named routes for implementing verb-oriented controllers for a singleton resource. # Creates named routes for implementing verb-oriented controllers for a singleton \resource.
# A singleton resource is global to its current context. For unnested singleton resources, # A singleton \resource is global to its current context. For unnested singleton \resources,
# the resource is global to the current user visiting the application, such as a user's # the \resource is global to the current user visiting the application, such as a user's
# /account profile. For nested singleton resources, the resource is global to its parent # <tt>/account</tt> profile. For nested singleton \resources, the \resource is global to its parent
# resource, such as a <tt>projects</tt> resource that <tt>has_one :project_manager</tt>. # \resource, such as a <tt>projects</tt> \resource that <tt>has_one :project_manager</tt>.
# The <tt>project_manager</tt> should be mapped as a singleton resource under <tt>projects</tt>: # The <tt>project_manager</tt> should be mapped as a singleton \resource under <tt>projects</tt>:
# #
# map.resources :projects do |project| # map.resources :projects do |project|
# project.resource :project_manager # project.resource :project_manager
# end # end
# #
# See map.resources for general conventions. These are the main differences: # See +resources+ for general conventions. These are the main differences:
# * A singular name is given to map.resource. The default controller name is still taken from the plural name. # * A singular name is given to <tt>map.resource</tt>. The default controller name is still taken from the plural name.
# * To specify a custom plural name, use the <tt>:plural</tt> option. There is no <tt>:singular</tt> option. # * To specify a custom plural name, use the <tt>:plural</tt> option. There is no <tt>:singular</tt> option.
# * No default index route is created for the singleton resource controller. # * No default index route is created for the singleton \resource controller.
# * When nesting singleton resources, only the singular name is used as the path prefix (example: 'account/messages/1') # * When nesting singleton \resources, only the singular name is used as the path prefix (example: 'account/messages/1')
# #
# For example: # For example:
# #
@ -438,7 +478,7 @@ module ActionController
map_associations(resource, options) map_associations(resource, options)
if block_given? if block_given?
with_options(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :namespace => options[:namespace], &block) with_options(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :namespace => options[:namespace], :shallow => options[:shallow], &block)
end end
end end
end end
@ -455,30 +495,45 @@ module ActionController
map_associations(resource, options) map_associations(resource, options)
if block_given? if block_given?
with_options(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :namespace => options[:namespace], &block) with_options(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :namespace => options[:namespace], :shallow => options[:shallow], &block)
end end
end end
end end
def map_associations(resource, options) def map_associations(resource, options)
map_has_many_associations(resource, options.delete(:has_many), options) if options[:has_many]
path_prefix = "#{options.delete(:path_prefix)}#{resource.nesting_path_prefix}" path_prefix = "#{options.delete(:path_prefix)}#{resource.nesting_path_prefix}"
name_prefix = "#{options.delete(:name_prefix)}#{resource.nesting_name_prefix}" name_prefix = "#{options.delete(:name_prefix)}#{resource.nesting_name_prefix}"
Array(options[:has_many]).each do |association| Array(options[:has_one]).each do |association|
resources(association, :path_prefix => path_prefix, :name_prefix => name_prefix, :namespace => options[:namespace]) resource(association, :path_prefix => path_prefix, :name_prefix => name_prefix, :namespace => options[:namespace], :shallow => options[:shallow])
end
end end
Array(options[:has_one]).each do |association| def map_has_many_associations(resource, associations, options)
resource(association, :path_prefix => path_prefix, :name_prefix => name_prefix, :namespace => options[:namespace]) case associations
when Hash
associations.each do |association,has_many|
map_has_many_associations(resource, association, options.merge(:has_many => has_many))
end
when Array
associations.each do |association|
map_has_many_associations(resource, association, options)
end
when Symbol, String
resources(associations, :path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :namespace => options[:namespace], :shallow => options[:shallow], :has_many => options[:has_many])
else
end end
end end
def map_collection_actions(map, resource) def map_collection_actions(map, resource)
resource.collection_methods.each do |method, actions| resource.collection_methods.each do |method, actions|
actions.each do |action| actions.each do |action|
action_options = action_options_for(action, resource, method) [method].flatten.each do |m|
map.named_route("#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}", action_options) action_options = action_options_for(action, resource, m)
map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}.:format", action_options) map_named_routes(map, "#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}", action_options)
end
end end
end end
end end
@ -491,18 +546,15 @@ module ActionController
index_route_name << "_index" index_route_name << "_index"
end end
map.named_route(index_route_name, resource.path, index_action_options) map_named_routes(map, index_route_name, resource.path, index_action_options)
map.named_route("formatted_#{index_route_name}", "#{resource.path}.:format", index_action_options)
create_action_options = action_options_for("create", resource) create_action_options = action_options_for("create", resource)
map.connect(resource.path, create_action_options) map_unnamed_routes(map, resource.path, create_action_options)
map.connect("#{resource.path}.:format", create_action_options)
end end
def map_default_singleton_actions(map, resource) def map_default_singleton_actions(map, resource)
create_action_options = action_options_for("create", resource) create_action_options = action_options_for("create", resource)
map.connect(resource.path, create_action_options) map_unnamed_routes(map, resource.path, create_action_options)
map.connect("#{resource.path}.:format", create_action_options)
end end
def map_new_actions(map, resource) def map_new_actions(map, resource)
@ -510,11 +562,9 @@ module ActionController
actions.each do |action| actions.each do |action|
action_options = action_options_for(action, resource, method) action_options = action_options_for(action, resource, method)
if action == :new if action == :new
map.named_route("new_#{resource.name_prefix}#{resource.singular}", resource.new_path, action_options) map_named_routes(map, "new_#{resource.name_prefix}#{resource.singular}", resource.new_path, action_options)
map.named_route("formatted_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}.:format", action_options)
else else
map.named_route("#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}", action_options) map_named_routes(map, "#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}", action_options)
map.named_route("formatted_#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}.:format", action_options)
end end
end end
end end
@ -523,27 +573,35 @@ module ActionController
def map_member_actions(map, resource) def map_member_actions(map, resource)
resource.member_methods.each do |method, actions| resource.member_methods.each do |method, actions|
actions.each do |action| actions.each do |action|
action_options = action_options_for(action, resource, method) [method].flatten.each do |m|
action_options = action_options_for(action, resource, m)
action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash)
action_path ||= Base.resources_path_names[action] || action action_path ||= Base.resources_path_names[action] || action
map.named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action_path}", action_options) map_named_routes(map, "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action_path}", action_options)
map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action_path}.:format",action_options) end
end end
end end
show_action_options = action_options_for("show", resource) show_action_options = action_options_for("show", resource)
map.named_route("#{resource.name_prefix}#{resource.singular}", resource.member_path, show_action_options) map_named_routes(map, "#{resource.shallow_name_prefix}#{resource.singular}", resource.member_path, show_action_options)
map.named_route("formatted_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}.:format", show_action_options)
update_action_options = action_options_for("update", resource) update_action_options = action_options_for("update", resource)
map.connect(resource.member_path, update_action_options) map_unnamed_routes(map, resource.member_path, update_action_options)
map.connect("#{resource.member_path}.:format", update_action_options)
destroy_action_options = action_options_for("destroy", resource) destroy_action_options = action_options_for("destroy", resource)
map.connect(resource.member_path, destroy_action_options) map_unnamed_routes(map, resource.member_path, destroy_action_options)
map.connect("#{resource.member_path}.:format", destroy_action_options) end
def map_unnamed_routes(map, path_without_format, options)
map.connect(path_without_format, options)
map.connect("#{path_without_format}.:format", options)
end
def map_named_routes(map, name, path_without_format, options)
map.named_route(name, path_without_format, options)
map.named_route("formatted_#{name}", "#{path_without_format}.:format", options)
end end
def add_conditions_for(conditions, method) def add_conditions_for(conditions, method)
@ -555,6 +613,7 @@ module ActionController
def action_options_for(action, resource, method = nil) def action_options_for(action, resource, method = nil)
default_options = { :action => action.to_s } default_options = { :action => action.to_s }
require_id = !resource.kind_of?(SingletonResource) require_id = !resource.kind_of?(SingletonResource)
case default_options[:action] case default_options[:action]
when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements) when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements)
when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements) when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements)

View file

@ -1,26 +1,90 @@
require 'digest/md5' require 'digest/md5'
module ActionController module ActionController # :nodoc:
class AbstractResponse #:nodoc: # Represents an HTTP response generated by a controller action. One can use an
# ActionController::AbstractResponse object to retrieve the current state of the
# response, or customize the response. An AbstractResponse object can either
# represent a "real" HTTP response (i.e. one that is meant to be sent back to the
# web browser) or a test response (i.e. one that is generated from integration
# tests). See CgiResponse and TestResponse, respectively.
#
# AbstractResponse is mostly a Ruby on Rails framework implement detail, and should
# never be used directly in controllers. Controllers should use the methods defined
# in ActionController::Base instead. For example, if you want to set the HTTP
# response's content MIME type, then use ActionControllerBase#headers instead of
# AbstractResponse#headers.
#
# Nevertheless, integration tests may want to inspect controller responses in more
# detail, and that's when AbstractResponse can be useful for application developers.
# Integration test methods such as ActionController::Integration::Session#get and
# ActionController::Integration::Session#post return objects of type TestResponse
# (which are of course also of type AbstractResponse).
#
# For example, the following demo integration "test" prints the body of the
# controller response to the console:
#
# class DemoControllerTest < ActionController::IntegrationTest
# def test_print_root_path_to_console
# get('/')
# puts @response.body
# end
# end
class AbstractResponse
DEFAULT_HEADERS = { "Cache-Control" => "no-cache" } DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
attr_accessor :request attr_accessor :request
attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params, :layout
# The body content (e.g. HTML) of the response, as a String.
attr_accessor :body
# The headers of the response, as a Hash. It maps header names to header values.
attr_accessor :headers
attr_accessor :session, :cookies, :assigns, :template, :layout
attr_accessor :redirected_to, :redirected_to_method_params
delegate :default_charset, :to => 'ActionController::Base'
def initialize def initialize
@body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], [] @body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], []
end end
def status; headers['Status'] end
def status=(status) headers['Status'] = status end
def location; headers['Location'] end
def location=(url) headers['Location'] = url end
# Sets the HTTP response's content MIME type. For example, in the controller
# you could write this:
#
# response.content_type = "text/plain"
#
# If a character set has been defined for this response (see charset=) then
# the character set information will also be included in the content type
# information.
def content_type=(mime_type) def content_type=(mime_type)
self.headers["Content-Type"] = charset ? "#{mime_type}; charset=#{charset}" : mime_type self.headers["Content-Type"] =
if mime_type =~ /charset/ || (c = charset).nil?
mime_type.to_s
else
"#{mime_type}; charset=#{c}"
end
end end
# Returns the response's content MIME type, or nil if content type has been set.
def content_type def content_type
content_type = String(headers["Content-Type"] || headers["type"]).split(";")[0] content_type = String(headers["Content-Type"] || headers["type"]).split(";")[0]
content_type.blank? ? nil : content_type content_type.blank? ? nil : content_type
end end
def charset=(encoding) # Set the charset of the Content-Type header. Set to nil to remove it.
self.headers["Content-Type"] = "#{content_type || Mime::HTML}; charset=#{encoding}" # If no content type is set, it defaults to HTML.
def charset=(charset)
headers["Content-Type"] =
if charset
"#{content_type || Mime::HTML}; charset=#{charset}"
else
content_type || Mime::HTML.to_s
end
end end
def charset def charset
@ -28,30 +92,78 @@ module ActionController
charset.blank? ? nil : charset.strip.split("=")[1] charset.blank? ? nil : charset.strip.split("=")[1]
end end
def redirect(to_url, response_status) def last_modified
self.headers["Status"] = response_status if last = headers['Last-Modified']
self.headers["Location"] = to_url.gsub(/[\r\n]/, '') Time.httpdate(last)
end
end
self.body = "<html><body>You are being <a href=\"#{CGI.escapeHTML(to_url)}\">redirected</a>.</body></html>" def last_modified?
headers.include?('Last-Modified')
end
def last_modified=(utc_time)
headers['Last-Modified'] = utc_time.httpdate
end
def etag
headers['ETag']
end
def etag?
headers.include?('ETag')
end
def etag=(etag)
headers['ETag'] = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}")
end
def redirect(url, status)
self.status = status
self.location = url.gsub(/[\r\n]/, '')
self.body = "<html><body>You are being <a href=\"#{CGI.escapeHTML(url)}\">redirected</a>.</body></html>"
end
def sending_file?
headers["Content-Transfer-Encoding"] == "binary"
end
def assign_default_content_type_and_charset!
self.content_type ||= Mime::HTML
self.charset ||= default_charset unless sending_file?
end end
def prepare! def prepare!
assign_default_content_type_and_charset!
handle_conditional_get! handle_conditional_get!
convert_content_type!
set_content_length! set_content_length!
convert_content_type!
end end
private private
def handle_conditional_get! def handle_conditional_get!
if body.is_a?(String) && (headers['Status'] ? headers['Status'][0..2] == '200' : true) && !body.empty? if etag? || last_modified?
self.headers['ETag'] ||= %("#{Digest::MD5.hexdigest(body)}") set_conditional_cache_control!
self.headers['Cache-Control'] = 'private, max-age=0, must-revalidate' if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control'] elsif nonempty_ok_response?
self.etag = body
if request.headers['HTTP_IF_NONE_MATCH'] == headers['ETag'] if request && request.etag_matches?(etag)
self.headers['Status'] = '304 Not Modified' self.status = '304 Not Modified'
self.body = '' self.body = ''
end end
set_conditional_cache_control!
end
end
def nonempty_ok_response?
ok = !status || status[0..2] == '200'
ok && body.is_a?(String) && !body.empty?
end
def set_conditional_cache_control!
if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control']
headers['Cache-Control'] = 'private, max-age=0, must-revalidate'
end end
end end
@ -70,7 +182,9 @@ module ActionController
# Don't set the Content-Length for block-based bodies as that would mean reading it all into memory. Not nice # Don't set the Content-Length for block-based bodies as that would mean reading it all into memory. Not nice
# for, say, a 2GB streaming file. # for, say, a 2GB streaming file.
def set_content_length! def set_content_length!
self.headers["Content-Length"] = body.size unless body.respond_to?(:call) unless body.respond_to?(:call) || (status && status[0..2] == '304')
self.headers["Content-Length"] ||= body.size
end
end end
end end
end end

View file

@ -360,7 +360,6 @@ module ActionController
# * previous namespace is root: # * previous namespace is root:
# controller_relative_to( "posts", "anything_with_no_slashes" ) # =>"posts" # controller_relative_to( "posts", "anything_with_no_slashes" ) # =>"posts"
# #
def controller_relative_to(controller, previous) def controller_relative_to(controller, previous)
if controller.nil? then previous if controller.nil? then previous
elsif controller[0] == ?/ then controller[1..-1] elsif controller[0] == ?/ then controller[1..-1]
@ -370,7 +369,6 @@ module ActionController
end end
end end
Routes = RouteSet.new Routes = RouteSet.new
ActiveSupport::Inflector.module_eval do ActiveSupport::Inflector.module_eval do

View file

@ -48,14 +48,10 @@ module ActionController
end end
when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true) when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true)
when /\A\?(.*?)\?/ when /\A\?(.*?)\?/
returning segment = StaticSegment.new($1) do StaticSegment.new($1, :optional => true)
segment.is_optional = true
end
when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1) when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1)
when Regexp.new(separator_pattern) then when Regexp.new(separator_pattern) then
returning segment = DividerSegment.new($&) do DividerSegment.new($&, :optional => (optional_separators.include? $&))
segment.is_optional = (optional_separators.include? $&)
end
end end
[segment, $~.post_match] [segment, $~.post_match]
end end
@ -64,18 +60,18 @@ module ActionController
# segments are passed alongside in order to distinguish between default values # segments are passed alongside in order to distinguish between default values
# and requirements. # and requirements.
def divide_route_options(segments, options) def divide_route_options(segments, options)
options = options.dup options = options.except(:path_prefix, :name_prefix)
if options[:namespace] if options[:namespace]
options[:controller] = "#{options.delete(:namespace).sub(/\/$/, '')}/#{options[:controller]}" options[:controller] = "#{options.delete(:namespace).sub(/\/$/, '')}/#{options[:controller]}"
options.delete(:path_prefix)
options.delete(:name_prefix)
end end
requirements = (options.delete(:requirements) || {}).dup requirements = (options.delete(:requirements) || {}).dup
defaults = (options.delete(:defaults) || {}).dup defaults = (options.delete(:defaults) || {}).dup
conditions = (options.delete(:conditions) || {}).dup conditions = (options.delete(:conditions) || {}).dup
validate_route_conditions(conditions)
path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact
options.each do |key, value| options.each do |key, value|
hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements
@ -174,29 +170,31 @@ module ActionController
defaults, requirements, conditions = divide_route_options(segments, options) defaults, requirements, conditions = divide_route_options(segments, options)
requirements = assign_route_options(segments, defaults, requirements) requirements = assign_route_options(segments, defaults, requirements)
route = Route.new # TODO: Segments should be frozen on initialize
segments.each { |segment| segment.freeze }
route.segments = segments route = Route.new(segments, requirements, conditions)
route.requirements = requirements
route.conditions = conditions
if !route.significant_keys.include?(:action) && !route.requirements[:action]
route.requirements[:action] = "index"
route.significant_keys << :action
end
# Routes cannot use the current string interpolation method
# if there are user-supplied <tt>:requirements</tt> as the interpolation
# code won't raise RoutingErrors when generating
if options.key?(:requirements) || route.requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION
route.optimise = false
end
if !route.significant_keys.include?(:controller) if !route.significant_keys.include?(:controller)
raise ArgumentError, "Illegal route: the :controller must be specified!" raise ArgumentError, "Illegal route: the :controller must be specified!"
end end
route route.freeze
end
private
def validate_route_conditions(conditions)
if method = conditions[:method]
[method].flatten.each do |m|
if m == :head
raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers"
end
unless HTTP_METHODS.include?(m.to_sym)
raise ArgumentError, "Invalid HTTP method specified in route conditions: #{conditions.inspect}"
end
end
end
end end
end end
end end

View file

@ -20,6 +20,7 @@ module ActionController
class Optimiser class Optimiser
attr_reader :route, :kind attr_reader :route, :kind
def initialize(route, kind) def initialize(route, kind)
@route = route @route = route
@kind = kind @kind = kind
@ -76,7 +77,7 @@ module ActionController
elements << '#{request.host_with_port}' elements << '#{request.host_with_port}'
end end
elements << '#{request.relative_url_root if request.relative_url_root}' elements << '#{ActionController::Base.relative_url_root if ActionController::Base.relative_url_root}'
# The last entry in <tt>route.segments</tt> appears to *always* be a # The last entry in <tt>route.segments</tt> appears to *always* be a
# 'divider segment' for '/' but we have assertions to ensure that # 'divider segment' for '/' but we have assertions to ensure that
@ -102,9 +103,10 @@ module ActionController
end end
# This case uses almost the same code as positional arguments, # This case uses almost the same code as positional arguments,
# but add an args.last.to_query on the end # but add a question mark and args.last.to_query on the end,
# unless the last arg is empty
def generation_code def generation_code
super.insert(-2, '?#{args.last.to_query}') super.insert(-2, '#{\'?\' + args.last.to_query unless args.last.empty?}')
end end
# To avoid generating "http://localhost/?host=foo.example.com" we # To avoid generating "http://localhost/?host=foo.example.com" we

View file

@ -51,7 +51,6 @@ module ActionController
# 3) segm test for /users/:id # 3) segm test for /users/:id
# (jump to list index = 5) # (jump to list index = 5)
# 4) full test for /users/:id => here we are! # 4) full test for /users/:id => here we are!
class RouteSet class RouteSet
def recognize_path(path, environment={}) def recognize_path(path, environment={})
result = recognize_optimized(path, environment) and return result result = recognize_optimized(path, environment) and return result
@ -68,28 +67,6 @@ module ActionController
end end
end end
def recognize_optimized(path, env)
write_recognize_optimized
recognize_optimized(path, env)
end
def write_recognize_optimized
tree = segment_tree(routes)
body = generate_code(tree)
instance_eval %{
def recognize_optimized(path, env)
segments = to_plain_segments(path)
index = #{body}
return nil unless index
while index < routes.size
result = routes[index].recognize(path, env) and return result
index += 1
end
nil
end
}, __FILE__, __LINE__
end
def segment_tree(routes) def segment_tree(routes)
tree = [0] tree = [0]
@ -153,6 +130,45 @@ module ActionController
segments segments
end end
private
def write_recognize_optimized!
tree = segment_tree(routes)
body = generate_code(tree)
remove_recognize_optimized!
instance_eval %{
def recognize_optimized(path, env)
segments = to_plain_segments(path)
index = #{body}
return nil unless index
while index < routes.size
result = routes[index].recognize(path, env) and return result
index += 1
end
nil
end
}, __FILE__, __LINE__
end
def clear_recognize_optimized!
remove_recognize_optimized!
class << self
def recognize_optimized(path, environment)
write_recognize_optimized!
recognize_optimized(path, environment)
end
end
end
def remove_recognize_optimized!
if respond_to?(:recognize_optimized)
class << self
remove_method :recognize_optimized
end
end
end
end end
end end
end end

View file

@ -3,12 +3,26 @@ module ActionController
class Route #:nodoc: class Route #:nodoc:
attr_accessor :segments, :requirements, :conditions, :optimise attr_accessor :segments, :requirements, :conditions, :optimise
def initialize def initialize(segments = [], requirements = {}, conditions = {})
@segments = [] @segments = segments
@requirements = {} @requirements = requirements
@conditions = {} @conditions = conditions
if !significant_keys.include?(:action) && !requirements[:action]
@requirements[:action] = "index"
@significant_keys << :action
end
# Routes cannot use the current string interpolation method
# if there are user-supplied <tt>:requirements</tt> as the interpolation
# code won't raise RoutingErrors when generating
has_requirements = @segments.detect { |segment| segment.respond_to?(:regexp) && segment.regexp }
if has_requirements || @requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION
@optimise = false
else
@optimise = true @optimise = true
end end
end
# Indicates whether the routes should be optimised with the string interpolation # Indicates whether the routes should be optimised with the string interpolation
# version of the named routes methods. # version of the named routes methods.
@ -22,8 +36,103 @@ module ActionController
end.compact end.compact
end end
# Build a query string from the keys of the given hash. If +only_keys+
# is given (as an array), only the keys indicated will be used to build
# the query string. The query string will correctly build array parameter
# values.
def build_query_string(hash, only_keys = nil)
elements = []
(only_keys || hash.keys).each do |key|
if value = hash[key]
elements << value.to_query(key)
end
end
elements.empty? ? '' : "?#{elements.sort * '&'}"
end
# A route's parameter shell contains parameter values that are not in the
# route's path, but should be placed in the recognized hash.
#
# For example, +{:controller => 'pages', :action => 'show'} is the shell for the route:
#
# map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/
#
def parameter_shell
@parameter_shell ||= returning({}) do |shell|
requirements.each do |key, requirement|
shell[key] = requirement unless requirement.is_a? Regexp
end
end
end
# Return an array containing all the keys that are used in this route. This
# includes keys that appear inside the path, and keys that have requirements
# placed upon them.
def significant_keys
@significant_keys ||= returning([]) do |sk|
segments.each { |segment| sk << segment.key if segment.respond_to? :key }
sk.concat requirements.keys
sk.uniq!
end
end
# Return a hash of key/value pairs representing the keys in the route that
# have defaults, or which are specified by non-regexp requirements.
def defaults
@defaults ||= returning({}) do |hash|
segments.each do |segment|
next unless segment.respond_to? :default
hash[segment.key] = segment.default unless segment.default.nil?
end
requirements.each do |key,req|
next if Regexp === req || req.nil?
hash[key] = req
end
end
end
def matches_controller_and_action?(controller, action)
prepare_matching!
(@controller_requirement.nil? || @controller_requirement === controller) &&
(@action_requirement.nil? || @action_requirement === action)
end
def to_s
@to_s ||= begin
segs = segments.inject("") { |str,s| str << s.to_s }
"%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect]
end
end
# TODO: Route should be prepared and frozen on initialize
def freeze
unless frozen?
write_generation!
write_recognition!
prepare_matching!
parameter_shell
significant_keys
defaults
to_s
end
super
end
private
def requirement_for(key)
return requirements[key] if requirements.key? key
segments.each do |segment|
return segment.regexp if segment.respond_to?(:key) && segment.key == key
end
nil
end
# Write and compile a +generate+ method for this Route. # Write and compile a +generate+ method for this Route.
def write_generation def write_generation!
# Build the main body of the generation # Build the main body of the generation
body = "expired = false\n#{generation_extraction}\n#{generation_structure}" body = "expired = false\n#{generation_extraction}\n#{generation_structure}"
@ -76,13 +185,13 @@ module ActionController
end end
# Write and compile a +recognize+ method for this Route. # Write and compile a +recognize+ method for this Route.
def write_recognition def write_recognition!
# Create an if structure to extract the params from a match if it occurs. # Create an if structure to extract the params from a match if it occurs.
body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams" body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend" body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"
# Build the method declaration and compile it # Build the method declaration and compile it
method_decl = "def recognize(path, env={})\n#{body}\nend" method_decl = "def recognize(path, env = {})\n#{body}\nend"
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
method_decl method_decl
end end
@ -92,7 +201,7 @@ module ActionController
# recognition, not generation. # recognition, not generation.
def recognition_conditions def recognition_conditions
result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"] result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"]
result << "conditions[:method] === env[:method]" if conditions[:method] result << "[conditions[:method]].flatten.include?(env[:method])" if conditions[:method]
result result
end end
@ -116,20 +225,9 @@ module ActionController
extraction.compact extraction.compact
end end
# Write the real generation implementation and then resend the message.
def generate(options, hash, expire_on = {})
write_generation
generate options, hash, expire_on
end
def generate_extras(options, hash, expire_on = {})
write_generation
generate_extras options, hash, expire_on
end
# Generate the query string with any extra keys in the hash and append # Generate the query string with any extra keys in the hash and append
# it to the given path, returning the new path. # it to the given path, returning the new path.
def append_query_string(path, hash, query_keys=nil) def append_query_string(path, hash, query_keys = nil)
return nil unless path return nil unless path
query_keys ||= extra_keys(hash) query_keys ||= extra_keys(hash)
"#{path}#{build_query_string(hash, query_keys)}" "#{path}#{build_query_string(hash, query_keys)}"
@ -141,100 +239,17 @@ module ActionController
# do they include any keys that were implied in the route (like a # do they include any keys that were implied in the route (like a
# <tt>:controller</tt> that is required, but not explicitly used in the # <tt>:controller</tt> that is required, but not explicitly used in the
# text of the route.) # text of the route.)
def extra_keys(hash, recall={}) def extra_keys(hash, recall = {})
(hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
end end
# Build a query string from the keys of the given hash. If +only_keys+ def prepare_matching!
# is given (as an array), only the keys indicated will be used to build
# the query string. The query string will correctly build array parameter
# values.
def build_query_string(hash, only_keys = nil)
elements = []
(only_keys || hash.keys).each do |key|
if value = hash[key]
elements << value.to_query(key)
end
end
elements.empty? ? '' : "?#{elements.sort * '&'}"
end
# Write the real recognition implementation and then resend the message.
def recognize(path, environment={})
write_recognition
recognize path, environment
end
# A route's parameter shell contains parameter values that are not in the
# route's path, but should be placed in the recognized hash.
#
# For example, +{:controller => 'pages', :action => 'show'} is the shell for the route:
#
# map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/
#
def parameter_shell
@parameter_shell ||= returning({}) do |shell|
requirements.each do |key, requirement|
shell[key] = requirement unless requirement.is_a? Regexp
end
end
end
# Return an array containing all the keys that are used in this route. This
# includes keys that appear inside the path, and keys that have requirements
# placed upon them.
def significant_keys
@significant_keys ||= returning [] do |sk|
segments.each { |segment| sk << segment.key if segment.respond_to? :key }
sk.concat requirements.keys
sk.uniq!
end
end
# Return a hash of key/value pairs representing the keys in the route that
# have defaults, or which are specified by non-regexp requirements.
def defaults
@defaults ||= returning({}) do |hash|
segments.each do |segment|
next unless segment.respond_to? :default
hash[segment.key] = segment.default unless segment.default.nil?
end
requirements.each do |key,req|
next if Regexp === req || req.nil?
hash[key] = req
end
end
end
def matches_controller_and_action?(controller, action)
unless defined? @matching_prepared unless defined? @matching_prepared
@controller_requirement = requirement_for(:controller) @controller_requirement = requirement_for(:controller)
@action_requirement = requirement_for(:action) @action_requirement = requirement_for(:action)
@matching_prepared = true @matching_prepared = true
end end
(@controller_requirement.nil? || @controller_requirement === controller) &&
(@action_requirement.nil? || @action_requirement === action)
end end
def to_s
@to_s ||= begin
segs = segments.inject("") { |str,s| str << s.to_s }
"%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect]
end
end
protected
def requirement_for(key)
return requirements[key] if requirements.key? key
segments.each do |segment|
return segment.regexp if segment.respond_to?(:key) && segment.key == key
end
nil
end
end end
end end
end end

View file

@ -115,7 +115,7 @@ module ActionController
def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false) def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false)
reset! if regenerate reset! if regenerate
Array(destinations).each do |dest| Array(destinations).each do |dest|
dest.send! :include, @module dest.__send__(:include, @module)
end end
end end
@ -194,6 +194,8 @@ module ActionController
def initialize def initialize
self.routes = [] self.routes = []
self.named_routes = NamedRouteCollection.new self.named_routes = NamedRouteCollection.new
clear_recognize_optimized!
end end
# Subclasses and plugins may override this method to specify a different # Subclasses and plugins may override this method to specify a different
@ -215,7 +217,7 @@ module ActionController
@routes_by_controller = nil @routes_by_controller = nil
# This will force routing/recognition_optimization.rb # This will force routing/recognition_optimization.rb
# to refresh optimisations. # to refresh optimisations.
@compiled_recognize_optimized = nil clear_recognize_optimized!
end end
def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false)
@ -231,7 +233,6 @@ module ActionController
Routing.use_controllers! nil # Clear the controller cache so we may discover new ones Routing.use_controllers! nil # Clear the controller cache so we may discover new ones
clear! clear!
load_routes! load_routes!
install_helpers
end end
# reload! will always force a reload whereas load checks the timestamp first # reload! will always force a reload whereas load checks the timestamp first
@ -352,7 +353,7 @@ module ActionController
if generate_all if generate_all
# Used by caching to expire all paths for a resource # Used by caching to expire all paths for a resource
return routes.collect do |route| return routes.collect do |route|
route.send!(method, options, merged, expire_on) route.__send__(method, options, merged, expire_on)
end.compact end.compact
end end
@ -360,7 +361,7 @@ module ActionController
routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }] routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }]
routes.each do |route| routes.each do |route|
results = route.send!(method, options, merged, expire_on) results = route.__send__(method, options, merged, expire_on)
return results if results && (!results.is_a?(Array) || results.first) return results if results && (!results.is_a?(Array) || results.first)
end end
end end

View file

@ -1,4 +1,3 @@
class Object class Object
def to_param def to_param
to_s to_s

View file

@ -2,13 +2,15 @@ module ActionController
module Routing module Routing
class Segment #:nodoc: class Segment #:nodoc:
RESERVED_PCHAR = ':@&=+$,;' RESERVED_PCHAR = ':@&=+$,;'
UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}"
UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
# TODO: Convert :is_optional accessor to read only
attr_accessor :is_optional attr_accessor :is_optional
alias_method :optional?, :is_optional alias_method :optional?, :is_optional
def initialize def initialize
self.is_optional = false @is_optional = false
end end
def extraction_code def extraction_code
@ -63,12 +65,14 @@ module ActionController
end end
class StaticSegment < Segment #:nodoc: class StaticSegment < Segment #:nodoc:
attr_accessor :value, :raw attr_reader :value, :raw
alias_method :raw?, :raw alias_method :raw?, :raw
def initialize(value = nil) def initialize(value = nil, options = {})
super() super()
self.value = value @value = value
@raw = options[:raw] if options.key?(:raw)
@is_optional = options[:optional] if options.key?(:optional)
end end
def interpolation_chunk def interpolation_chunk
@ -97,10 +101,8 @@ module ActionController
end end
class DividerSegment < StaticSegment #:nodoc: class DividerSegment < StaticSegment #:nodoc:
def initialize(value = nil) def initialize(value = nil, options = {})
super(value) super(value, {:raw => true, :optional => true}.merge(options))
self.raw = true
self.is_optional = true
end end
def optionality_implied? def optionality_implied?
@ -109,13 +111,17 @@ module ActionController
end end
class DynamicSegment < Segment #:nodoc: class DynamicSegment < Segment #:nodoc:
attr_accessor :key, :default, :regexp attr_reader :key
# TODO: Convert these accessors to read only
attr_accessor :default, :regexp
def initialize(key = nil, options = {}) def initialize(key = nil, options = {})
super() super()
self.key = key @key = key
self.default = options[:default] if options.key? :default @default = options[:default] if options.key?(:default)
self.is_optional = true if options[:optional] || options.key?(:default) @regexp = options[:regexp] if options.key?(:regexp)
@is_optional = true if options[:optional] || options.key?(:default)
end end
def to_s def to_s
@ -130,6 +136,7 @@ module ActionController
def extract_value def extract_value
"#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}" "#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}"
end end
def value_check def value_check
if default # Then we know it won't be nil if default # Then we know it won't be nil
"#{value_regexp.inspect} =~ #{local_name}" if regexp "#{value_regexp.inspect} =~ #{local_name}" if regexp
@ -141,6 +148,7 @@ module ActionController
"#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}" "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}"
end end
end end
def expiry_statement def expiry_statement
"expired, hash = true, options if !expired && expire_on[:#{key}]" "expired, hash = true, options if !expired && expire_on[:#{key}]"
end end
@ -152,7 +160,7 @@ module ActionController
s << "\n#{expiry_statement}" s << "\n#{expiry_statement}"
end end
def interpolation_chunk(value_code = "#{local_name}") def interpolation_chunk(value_code = local_name)
"\#{CGI.escape(#{value_code}.to_s)}" "\#{CGI.escape(#{value_code}.to_s)}"
end end
@ -214,7 +222,6 @@ module ActionController
def regexp_has_modifiers? def regexp_has_modifiers?
regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0 regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0
end end
end end
class ControllerSegment < DynamicSegment #:nodoc: class ControllerSegment < DynamicSegment #:nodoc:
@ -224,9 +231,10 @@ module ActionController
end end
# Don't URI.escape the controller name since it may contain slashes. # Don't URI.escape the controller name since it may contain slashes.
def interpolation_chunk(value_code = "#{local_name}") def interpolation_chunk(value_code = local_name)
"\#{#{value_code}.to_s}" "\#{#{value_code}.to_s}"
end end
# Make sure controller names like Admin/Content are correctly normalized to # Make sure controller names like Admin/Content are correctly normalized to
# admin/content # admin/content
def extract_value def extract_value
@ -243,12 +251,12 @@ module ActionController
end end
class PathSegment < DynamicSegment #:nodoc: class PathSegment < DynamicSegment #:nodoc:
def interpolation_chunk(value_code = "#{local_name}") def interpolation_chunk(value_code = local_name)
"\#{#{value_code}}" "\#{#{value_code}}"
end end
def extract_value def extract_value
"#{local_name} = hash[:#{key}] && Array(hash[:#{key}]).collect { |path_component| CGI.escape(path_component.to_param, ActionController::Routing::Segment::UNSAFE_PCHAR) }.to_param #{"|| #{default.inspect}" if default}" "#{local_name} = hash[:#{key}] && Array(hash[:#{key}]).collect { |path_component| CGI.escape(path_component.to_param) }.to_param #{"|| #{default.inspect}" if default}"
end end
def default def default

View file

@ -70,7 +70,8 @@ class CGI::Session::CookieStore
'path' => options['session_path'], 'path' => options['session_path'],
'domain' => options['session_domain'], 'domain' => options['session_domain'],
'expires' => options['session_expires'], 'expires' => options['session_expires'],
'secure' => options['session_secure'] 'secure' => options['session_secure'],
'http_only' => options['session_http_only']
} }
# Set no_hidden and no_cookies since the session id is unused and we # Set no_hidden and no_cookies since the session id is unused and we
@ -129,7 +130,7 @@ class CGI::Session::CookieStore
private private
# Marshal a session hash into safe cookie data. Include an integrity hash. # Marshal a session hash into safe cookie data. Include an integrity hash.
def marshal(session) def marshal(session)
data = ActiveSupport::Base64.encode64(Marshal.dump(session)).chop data = ActiveSupport::Base64.encode64s(Marshal.dump(session))
"#{data}--#{generate_digest(data)}" "#{data}--#{generate_digest(data)}"
end end

View file

@ -1,4 +1,4 @@
#!/usr/local/bin/ruby -w #!/usr/bin/env ruby
# This is a really simple session storage daemon, basically just a hash, # This is a really simple session storage daemon, basically just a hash,
# which is enabled for DRb access. # which is enabled for DRb access.

View file

@ -60,6 +60,10 @@ module ActionController #:nodoc:
# # the session will only work over HTTPS, but only for the foo action # # the session will only work over HTTPS, but only for the foo action
# session :only => :foo, :session_secure => true # session :only => :foo, :session_secure => true
# #
# # the session by default uses HttpOnly sessions for security reasons.
# # this can be switched off.
# session :only => :foo, :session_http_only => false
#
# # the session will only be disabled for 'foo', and only if it is # # the session will only be disabled for 'foo', and only if it is
# # requested as a web service # # requested as a web service
# session :off, :only => :foo, # session :off, :only => :foo,
@ -86,14 +90,14 @@ module ActionController #:nodoc:
raise ArgumentError, "only one of either :only or :except are allowed" raise ArgumentError, "only one of either :only or :except are allowed"
end end
write_inheritable_array("session_options", [options]) write_inheritable_array(:session_options, [options])
end end
# So we can declare session options in the Rails initializer. # So we can declare session options in the Rails initializer.
alias_method :session=, :session alias_method :session=, :session
def cached_session_options #:nodoc: def cached_session_options #:nodoc:
@session_options ||= read_inheritable_attribute("session_options") || [] @session_options ||= read_inheritable_attribute(:session_options) || []
end end
def session_options_for(request, action) #:nodoc: def session_options_for(request, action) #:nodoc:

View file

@ -12,19 +12,21 @@ module ActionController #:nodoc:
X_SENDFILE_HEADER = 'X-Sendfile'.freeze X_SENDFILE_HEADER = 'X-Sendfile'.freeze
protected protected
# Sends the file by streaming it 4096 bytes at a time. This way the # Sends the file, by default streaming it 4096 bytes at a time. This way the
# whole file doesn't need to be read into memory at once. This makes # whole file doesn't need to be read into memory at once. This makes it
# it feasible to send even large files. # feasible to send even large files. You can optionally turn off streaming
# and send the whole file at once.
# #
# Be careful to sanitize the path parameter if it coming from a web # Be careful to sanitize the path parameter if it is coming from a web
# page. <tt>send_file(params[:path])</tt> allows a malicious user to # page. <tt>send_file(params[:path])</tt> allows a malicious user to
# download any file on your server. # download any file on your server.
# #
# Options: # Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use. # * <tt>:filename</tt> - suggests a filename for the browser to use.
# Defaults to <tt>File.basename(path)</tt>. # Defaults to <tt>File.basename(path)</tt>.
# * <tt>:type</tt> - specifies an HTTP content type. # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'.
# Defaults to 'application/octet-stream'. # * <tt>:length</tt> - used to manually override the length (in bytes) of the content that
# is going to be sent to the client. Defaults to <tt>File.size(path)</tt>.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default). # Valid values are 'inline' and 'attachment' (default).
# * <tt>:stream</tt> - whether to send the file to the user agent as it is read (+true+) # * <tt>:stream</tt> - whether to send the file to the user agent as it is read (+true+)
@ -35,6 +37,12 @@ module ActionController #:nodoc:
# * <tt>:url_based_filename</tt> - set to +true+ if you want the browser guess the filename from # * <tt>:url_based_filename</tt> - set to +true+ if you want the browser guess the filename from
# the URL, which is necessary for i18n filenames on certain browsers # the URL, which is necessary for i18n filenames on certain browsers
# (setting <tt>:filename</tt> overrides this option). # (setting <tt>:filename</tt> overrides this option).
# * <tt>:x_sendfile</tt> - uses X-Sendfile to send the file when set to +true+. This is currently
# only available with Lighttpd/Apache2 and specific modules installed and activated. Since this
# uses the web server to send the file, this may lower memory consumption on your server and
# it will not block your application for further requests.
# See http://blog.lighttpd.net/articles/2006/07/02/x-sendfile and
# http://tn123.ath.cx/mod_xsendfile/ for details. Defaults to +false+.
# #
# The default Content-Type and Content-Disposition headers are # The default Content-Type and Content-Disposition headers are
# set to download arbitrary binary files in as many browsers as # set to download arbitrary binary files in as many browsers as
@ -99,8 +107,7 @@ module ActionController #:nodoc:
# #
# Options: # Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use. # * <tt>:filename</tt> - suggests a filename for the browser to use.
# * <tt>:type</tt> - specifies an HTTP content type. # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'.
# Defaults to 'application/octet-stream'.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default). # Valid values are 'inline' and 'attachment' (default).
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'. # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.

View file

@ -6,6 +6,6 @@
</h1> </h1>
<pre><%=h @exception.clean_message %></pre> <pre><%=h @exception.clean_message %></pre>
<%= render_file(@rescues_path + "/_trace.erb", false) %> <%= render(:file => @rescues_path + "/_trace.erb") %>
<%= render_file(@rescues_path + "/_request_and_response.erb", false) %> <%= render(:file => @rescues_path + "/_request_and_response.erb") %>

View file

@ -15,7 +15,7 @@
<% @real_exception = @exception <% @real_exception = @exception
@exception = @exception.original_exception || @exception %> @exception = @exception.original_exception || @exception %>
<%= render_file(@rescues_path + "/_trace.erb", false) %> <%= render(:file => @rescues_path + "/_trace.erb") %>
<% @exception = @real_exception %> <% @exception = @real_exception %>
<%= render_file(@rescues_path + "/_request_and_response.erb", false) %> <%= render(:file => @rescues_path + "/_request_and_response.erb") %>

View file

@ -15,6 +15,65 @@ module ActionController
end end
end end
# Superclass for ActionController functional tests. Functional tests allow you to
# test a single controller action per test method. This should not be confused with
# integration tests (see ActionController::IntegrationTest), which are more like
# "stories" that can involve multiple controllers and mutliple actions (i.e. multiple
# different HTTP requests).
#
# == Basic example
#
# Functional tests are written as follows:
# 1. First, one uses the +get+, +post+, +put+, +delete+ or +head+ method to simulate
# an HTTP request.
# 2. Then, one asserts whether the current state is as expected. "State" can be anything:
# the controller's HTTP response, the database contents, etc.
#
# For example:
#
# class BooksControllerTest < ActionController::TestCase
# def test_create
# # Simulate a POST response with the given HTTP parameters.
# post(:create, :book => { :title => "Love Hina" })
#
# # Assert that the controller tried to redirect us to
# # the created book's URI.
# assert_response :found
#
# # Assert that the controller really put the book in the database.
# assert_not_nil Book.find_by_title("Love Hina")
# end
# end
#
# == Special instance variables
#
# ActionController::TestCase will also automatically provide the following instance
# variables for use in the tests:
#
# <b>@controller</b>::
# The controller instance that will be tested.
# <b>@request</b>::
# An ActionController::TestRequest, representing the current HTTP
# request. You can modify this object before sending the HTTP request. For example,
# you might want to set some session properties before sending a GET request.
# <b>@response</b>::
# An ActionController::TestResponse object, representing the response
# of the last HTTP response. In the above example, <tt>@response</tt> becomes valid
# after calling +post+. If the various assert methods are not sufficient, then you
# may use this object to inspect the HTTP response in detail.
#
# (Earlier versions of Rails required each functional test to subclass
# Test::Unit::TestCase and define @controller, @request, @response in +setup+.)
#
# == Controller is automatically inferred
#
# ActionController::TestCase will automatically infer the controller under test
# from the test class name. If the controller cannot be inferred from the test
# class name, you can explicity set it with +tests+.
#
# class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase
# tests WidgetController
# end
class TestCase < ActiveSupport::TestCase class TestCase < ActiveSupport::TestCase
# When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline # When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline
# (bystepping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular # (bystepping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular
@ -25,7 +84,7 @@ module ActionController
module RaiseActionExceptions module RaiseActionExceptions
attr_accessor :exception attr_accessor :exception
def rescue_action(e) def rescue_action_without_handler(e)
self.exception = e self.exception = e
if request.remote_addr == "0.0.0.0" if request.remote_addr == "0.0.0.0"
@ -41,6 +100,8 @@ module ActionController
@@controller_class = nil @@controller_class = nil
class << self class << self
# Sets the controller class name. Useful if the name can't be inferred from test class.
# Expects +controller_class+ as a constant. Example: <tt>tests WidgetController</tt>.
def tests(controller_class) def tests(controller_class)
self.controller_class = controller_class self.controller_class = controller_class
end end
@ -73,6 +134,9 @@ module ActionController
@controller = self.class.controller_class.new @controller = self.class.controller_class.new
@controller.request = @request = TestRequest.new @controller.request = @request = TestRequest.new
@response = TestResponse.new @response = TestResponse.new
@controller.params = {}
@controller.send(:initialize_current_url)
end end
# Cause the action to be rescued according to the regular rules for rescue_action when the visitor is not local # Cause the action to be rescued according to the regular rules for rescue_action when the visitor is not local

View file

@ -3,6 +3,8 @@ require 'action_controller/test_case'
module ActionController #:nodoc: module ActionController #:nodoc:
class Base class Base
attr_reader :assigns
# Process a test request called with a TestRequest object. # Process a test request called with a TestRequest object.
def self.process_test(request) def self.process_test(request)
new.process_test(request) new.process_test(request)
@ -14,7 +16,12 @@ module ActionController #:nodoc:
def process_with_test(*args) def process_with_test(*args)
returning process_without_test(*args) do returning process_without_test(*args) do
add_variables_to_assigns @assigns = {}
(instance_variable_names - @@protected_instance_variables).each do |var|
value = instance_variable_get(var)
@assigns[var[1..-1]] = value
response.template.assigns[var[1..-1]] = value if response
end
end end
end end
@ -23,7 +30,7 @@ module ActionController #:nodoc:
class TestRequest < AbstractRequest #:nodoc: class TestRequest < AbstractRequest #:nodoc:
attr_accessor :cookies, :session_options attr_accessor :cookies, :session_options
attr_accessor :query_parameters, :request_parameters, :path, :session, :env attr_accessor :query_parameters, :request_parameters, :path, :session
attr_accessor :host, :user_agent attr_accessor :host, :user_agent
def initialize(query_parameters = nil, request_parameters = nil, session = nil) def initialize(query_parameters = nil, request_parameters = nil, session = nil)
@ -42,7 +49,7 @@ module ActionController #:nodoc:
end end
# Wraps raw_post in a StringIO. # Wraps raw_post in a StringIO.
def body def body_stream #:nodoc:
StringIO.new(raw_post) StringIO.new(raw_post)
end end
@ -54,7 +61,7 @@ module ActionController #:nodoc:
def port=(number) def port=(number)
@env["SERVER_PORT"] = number.to_i @env["SERVER_PORT"] = number.to_i
@port_as_int = nil port(true)
end end
def action=(action_name) def action=(action_name)
@ -68,6 +75,8 @@ module ActionController #:nodoc:
@env["REQUEST_URI"] = value @env["REQUEST_URI"] = value
@request_uri = nil @request_uri = nil
@path = nil @path = nil
request_uri(true)
path(true)
end end
def request_uri=(uri) def request_uri=(uri)
@ -77,21 +86,26 @@ module ActionController #:nodoc:
def accept=(mime_types) def accept=(mime_types)
@env["HTTP_ACCEPT"] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",") @env["HTTP_ACCEPT"] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",")
accepts(true)
end
def if_modified_since=(last_modified)
@env["HTTP_IF_MODIFIED_SINCE"] = last_modified
end
def if_none_match=(etag)
@env["HTTP_IF_NONE_MATCH"] = etag
end end
def remote_addr=(addr) def remote_addr=(addr)
@env['REMOTE_ADDR'] = addr @env['REMOTE_ADDR'] = addr
end end
def remote_addr def request_uri(*args)
@env['REMOTE_ADDR']
end
def request_uri
@request_uri || super @request_uri || super
end end
def path def path(*args)
@path || super @path || super
end end
@ -119,11 +133,7 @@ module ActionController #:nodoc:
self.request_parameters = {} self.request_parameters = {}
self.query_parameters = {} self.query_parameters = {}
self.path_parameters = {} self.path_parameters = {}
@request_method, @accepts, @content_type = nil, nil, nil unmemoize_all
end
def referer
@env["HTTP_REFERER"]
end end
private private
@ -157,16 +167,16 @@ module ActionController #:nodoc:
module TestResponseBehavior #:nodoc: module TestResponseBehavior #:nodoc:
# The response code of the request # The response code of the request
def response_code def response_code
headers['Status'][0,3].to_i rescue 0 status[0,3].to_i rescue 0
end end
# Returns a String to ensure compatibility with Net::HTTPResponse # Returns a String to ensure compatibility with Net::HTTPResponse
def code def code
headers['Status'].to_s.split(' ')[0] status.to_s.split(' ')[0]
end end
def message def message
headers['Status'].to_s.split(' ',2)[1] status.to_s.split(' ',2)[1]
end end
# Was the response successful? # Was the response successful?
@ -205,24 +215,13 @@ module ActionController #:nodoc:
p.match(redirect_url) != nil p.match(redirect_url) != nil
end end
# Returns the template path of the file which was used to # Returns the template of the file which was used to
# render this response (or nil) # render this response (or nil)
def rendered_file(with_controller=false) def rendered_template
unless template.first_render.nil? template.send(:_first_render)
unless with_controller
template.first_render
else
template.first_render.split('/').last || template.first_render
end
end
end end
# Was this template rendered by a file? # A shortcut to the flash. Returns an empty hash if no session flash exists.
def rendered_with_file?
!rendered_file.nil?
end
# A shortcut to the flash. Returns an empyt hash if no session flash exists.
def flash def flash
session['flash'] || {} session['flash'] || {}
end end
@ -277,8 +276,19 @@ module ActionController #:nodoc:
end end
end end
class TestResponse < AbstractResponse #:nodoc: # Integration test methods such as ActionController::Integration::Session#get
# and ActionController::Integration::Session#post return objects of class
# TestResponse, which represent the HTTP response results of the requested
# controller actions.
#
# See AbstractResponse for more information on controller response objects.
class TestResponse < AbstractResponse
include TestResponseBehavior include TestResponseBehavior
def recycle!
headers.delete('ETag')
headers.delete('Last-Modified')
end
end end
class TestSession #:nodoc: class TestSession #:nodoc:
@ -352,13 +362,14 @@ module ActionController #:nodoc:
alias local_path path alias local_path path
def method_missing(method_name, *args, &block) #:nodoc: def method_missing(method_name, *args, &block) #:nodoc:
@tempfile.send!(method_name, *args, &block) @tempfile.__send__(method_name, *args, &block)
end end
end end
module TestProcess module TestProcess
def self.included(base) def self.included(base)
# execute the request simulating a specific HTTP method and set/volley the response # execute the request simulating a specific HTTP method and set/volley the response
# TODO: this should be un-DRY'ed for the sake of API documentation.
%w( get post put delete head ).each do |method| %w( get post put delete head ).each do |method|
base.class_eval <<-EOV, __FILE__, __LINE__ base.class_eval <<-EOV, __FILE__, __LINE__
def #{method}(action, parameters = nil, session = nil, flash = nil) def #{method}(action, parameters = nil, session = nil, flash = nil)
@ -380,6 +391,7 @@ module ActionController #:nodoc:
end end
@request.recycle! @request.recycle!
@response.recycle!
@html_document = nil @html_document = nil
@request.env['REQUEST_METHOD'] ||= "GET" @request.env['REQUEST_METHOD'] ||= "GET"
@ -397,24 +409,13 @@ module ActionController #:nodoc:
def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil) def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
@request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
@request.env['HTTP_ACCEPT'] = 'text/javascript, text/html, application/xml, text/xml, */*' @request.env['HTTP_ACCEPT'] = 'text/javascript, text/html, application/xml, text/xml, */*'
returning send!(request_method, action, parameters, session, flash) do returning __send__(request_method, action, parameters, session, flash) do
@request.env.delete 'HTTP_X_REQUESTED_WITH' @request.env.delete 'HTTP_X_REQUESTED_WITH'
@request.env.delete 'HTTP_ACCEPT' @request.env.delete 'HTTP_ACCEPT'
end end
end end
alias xhr :xml_http_request alias xhr :xml_http_request
def follow_redirect
redirected_controller = @response.redirected_to[:controller]
if redirected_controller && redirected_controller != @controller.controller_name
raise "Can't follow redirects outside of current controller (from #{@controller.controller_name} to #{redirected_controller})"
end
get(@response.redirected_to.delete(:action), @response.redirected_to.stringify_keys)
end
deprecate :follow_redirect => "If you wish to follow redirects, you should use integration tests"
def assigns(key = nil) def assigns(key = nil)
if key.nil? if key.nil?
@response.template.assigns @response.template.assigns
@ -441,7 +442,7 @@ module ActionController #:nodoc:
def build_request_uri(action, parameters) def build_request_uri(action, parameters)
unless @request.env['REQUEST_URI'] unless @request.env['REQUEST_URI']
options = @controller.send!(:rewrite_options, parameters) options = @controller.__send__(:rewrite_options, parameters)
options.update(:only_path => true, :action => action) options.update(:only_path => true, :action => action)
url = ActionController::UrlRewriter.new(@request, parameters) url = ActionController::UrlRewriter.new(@request, parameters)
@ -463,8 +464,11 @@ module ActionController #:nodoc:
end end
def method_missing(selector, *args) def method_missing(selector, *args)
return @controller.send!(selector, *args) if ActionController::Routing::Routes.named_routes.helpers.include?(selector) if ActionController::Routing::Routes.named_routes.helpers.include?(selector)
return super @controller.send(selector, *args)
else
super
end
end end
# Shortcut for <tt>ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + path, type)</tt>: # Shortcut for <tt>ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + path, type)</tt>:

View file

@ -0,0 +1,13 @@
module ActionController
module Translation
def translate(*args)
I18n.translate *args
end
alias :t :translate
def localize(*args)
I18n.localize *args
end
alias :l :localize
end
end

View file

@ -1,19 +1,96 @@
module ActionController module ActionController
# Write URLs from arbitrary places in your codebase, such as your mailers. # In <b>routes.rb</b> one defines URL-to-controller mappings, but the reverse
# is also possible: an URL can be generated from one of your routing definitions.
# URL generation functionality is centralized in this module.
# #
# Example: # See ActionController::Routing and ActionController::Resources for general
# information about routing and routes.rb.
# #
# class MyMailer # <b>Tip:</b> If you need to generate URLs from your models or some other place,
# include ActionController::UrlWriter # then ActionController::UrlWriter is what you're looking for. Read on for
# default_url_options[:host] = 'www.basecamphq.com' # an introduction.
# #
# def signup_url(token) # == URL generation from parameters
# url_for(:controller => 'signup', action => 'index', :token => token) #
# As you may know, some functions - such as ActionController::Base#url_for
# and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set
# of parameters. For example, you've probably had the chance to write code
# like this in one of your views:
#
# <%= link_to('Click here', :controller => 'users',
# :action => 'new', :message => 'Welcome!') %>
#
# #=> Generates a link to: /users/new?message=Welcome%21
#
# link_to, and all other functions that require URL generation functionality,
# actually use ActionController::UrlWriter under the hood. And in particular,
# they use the ActionController::UrlWriter#url_for method. One can generate
# the same path as the above example by using the following code:
#
# include UrlWriter
# url_for(:controller => 'users',
# :action => 'new',
# :message => 'Welcome!',
# :only_path => true)
# # => "/users/new?message=Welcome%21"
#
# Notice the <tt>:only_path => true</tt> part. This is because UrlWriter has no
# information about the website hostname that your Rails app is serving. So if you
# want to include the hostname as well, then you must also pass the <tt>:host</tt>
# argument:
#
# include UrlWriter
# url_for(:controller => 'users',
# :action => 'new',
# :message => 'Welcome!',
# :host => 'www.example.com') # Changed this.
# # => "http://www.example.com/users/new?message=Welcome%21"
#
# By default, all controllers and views have access to a special version of url_for,
# that already knows what the current hostname is. So if you use url_for in your
# controllers or your views, then you don't need to explicitly pass the <tt>:host</tt>
# argument.
#
# For convenience reasons, mailers provide a shortcut for ActionController::UrlWriter#url_for.
# So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlWriter#url_for'
# in full. However, mailers don't have hostname information, and what's why you'll still
# have to specify the <tt>:host</tt> argument when generating URLs in mailers.
#
#
# == URL generation for named routes
#
# UrlWriter also allows one to access methods that have been auto-generated from
# named routes. For example, suppose that you have a 'users' resource in your
# <b>routes.rb</b>:
#
# map.resources :users
#
# This generates, among other things, the method <tt>users_path</tt>. By default,
# this method is accessible from your controllers, views and mailers. If you need
# to access this auto-generated method from other places (such as a model), then
# you can do that in two ways.
#
# The first way is to include ActionController::UrlWriter in your class:
#
# class User < ActiveRecord::Base
# include ActionController::UrlWriter # !!!
#
# def name=(value)
# write_attribute('name', value)
# write_attribute('base_uri', users_path) # !!!
# end # end
# end # end
# #
# In addition to providing +url_for+, named routes are also accessible after # The second way is to access them through ActionController::UrlWriter.
# including UrlWriter. # The autogenerated named routes methods are available as class methods:
#
# class User < ActiveRecord::Base
# def name=(value)
# write_attribute('name', value)
# path = ActionController::UrlWriter.users_path # !!!
# write_attribute('base_uri', path) # !!!
# end
# end
module UrlWriter module UrlWriter
# The default options for urls written by this writer. Typically a <tt>:host</tt> # The default options for urls written by this writer. Typically a <tt>:host</tt>
# pair is provided. # pair is provided.
@ -37,7 +114,7 @@ module ActionController
# * <tt>:port</tt> - Optionally specify the port to connect to. # * <tt>:port</tt> - Optionally specify the port to connect to.
# * <tt>:anchor</tt> - An anchor name to be appended to the path. # * <tt>:anchor</tt> - An anchor name to be appended to the path.
# * <tt>:skip_relative_url_root</tt> - If true, the url is not constructed using the # * <tt>:skip_relative_url_root</tt> - If true, the url is not constructed using the
# +relative_url_root+ set in ActionController::AbstractRequest.relative_url_root. # +relative_url_root+ set in ActionController::Base.relative_url_root.
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/" # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/"
# #
# Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to # Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to
@ -67,7 +144,7 @@ module ActionController
[:protocol, :host, :port, :skip_relative_url_root].each { |k| options.delete(k) } [:protocol, :host, :port, :skip_relative_url_root].each { |k| options.delete(k) }
end end
trailing_slash = options.delete(:trailing_slash) if options.key?(:trailing_slash) trailing_slash = options.delete(:trailing_slash) if options.key?(:trailing_slash)
url << ActionController::AbstractRequest.relative_url_root.to_s unless options[:skip_relative_url_root] url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root]
anchor = "##{CGI.escape options.delete(:anchor).to_param.to_s}" if options[:anchor] anchor = "##{CGI.escape options.delete(:anchor).to_param.to_s}" if options[:anchor]
generated = Routing::Routes.generate(options, {}) generated = Routing::Routes.generate(options, {})
url << (trailing_slash ? generated.sub(/\?|\z/) { "/" + $& } : generated) url << (trailing_slash ? generated.sub(/\?|\z/) { "/" + $& } : generated)
@ -108,7 +185,7 @@ module ActionController
end end
path = rewrite_path(options) path = rewrite_path(options)
rewritten_url << @request.relative_url_root.to_s unless options[:skip_relative_url_root] rewritten_url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root]
rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
rewritten_url << "##{options[:anchor]}" if options[:anchor] rewritten_url << "##{options[:anchor]}" if options[:anchor]

View file

@ -150,7 +150,14 @@ module HTML #:nodoc:
end end
if scanner.skip(/!\[CDATA\[/) if scanner.skip(/!\[CDATA\[/)
scanner.scan_until(/\]\]>/) unless scanner.skip_until(/\]\]>/)
if strict
raise "expected ]]> (got #{scanner.rest.inspect} for #{content})"
else
scanner.skip_until(/\Z/)
end
end
return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, '')) return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, ''))
end end
@ -265,7 +272,7 @@ module HTML #:nodoc:
# itself. # itself.
class CDATA < Text #:nodoc: class CDATA < Text #:nodoc:
def to_s def to_s
"<![CDATA[#{super}]>" "<![CDATA[#{super}]]>"
end end
end end

View file

@ -64,7 +64,7 @@ module HTML
# #
# When using a combination of the above, the element name comes first # When using a combination of the above, the element name comes first
# followed by identifier, class names, attributes, pseudo classes and # followed by identifier, class names, attributes, pseudo classes and
# negation in any order. Do not seprate these parts with spaces! # negation in any order. Do not separate these parts with spaces!
# Space separation is used for descendant selectors. # Space separation is used for descendant selectors.
# #
# For example: # For example:
@ -158,7 +158,7 @@ module HTML
# * <tt>:not(selector)</tt> -- Match the element only if the element does not # * <tt>:not(selector)</tt> -- Match the element only if the element does not
# match the simple selector. # match the simple selector.
# #
# As you can see, <tt>:nth-child<tt> pseudo class and its varient can get quite # As you can see, <tt>:nth-child<tt> pseudo class and its variant can get quite
# tricky and the CSS specification doesn't do a much better job explaining it. # tricky and the CSS specification doesn't do a much better job explaining it.
# But after reading the examples and trying a few combinations, it's easy to # But after reading the examples and trying a few combinations, it's easy to
# figure out. # figure out.

View file

@ -80,7 +80,7 @@ module ActionController #:nodoc:
# array (may also be a single value). # array (may also be a single value).
def verify(options={}) def verify(options={})
before_filter :only => options[:only], :except => options[:except] do |c| before_filter :only => options[:only], :except => options[:except] do |c|
c.send! :verify_action, options c.__send__ :verify_action, options
end end
end end
end end
@ -116,7 +116,7 @@ module ActionController #:nodoc:
end end
def apply_redirect_to(redirect_to_option) # :nodoc: def apply_redirect_to(redirect_to_option) # :nodoc:
(redirect_to_option.is_a?(Symbol) && redirect_to_option != :back) ? self.send!(redirect_to_option) : redirect_to_option (redirect_to_option.is_a?(Symbol) && redirect_to_option != :back) ? self.__send__(redirect_to_option) : redirect_to_option
end end
def apply_remaining_actions(options) # :nodoc: def apply_remaining_actions(options) # :nodoc:

View file

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

View file

@ -21,25 +21,33 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++ #++
require 'action_view/template_handler' begin
require 'action_view/template_handlers/compilable' require 'active_support'
require 'action_view/template_handlers/builder' rescue LoadError
require 'action_view/template_handlers/erb' activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
require 'action_view/template_handlers/rjs' if File.directory?(activesupport_path)
$:.unshift activesupport_path
require 'active_support'
end
end
require 'action_view/template_handlers'
require 'action_view/renderable'
require 'action_view/renderable_partial'
require 'action_view/template_finder'
require 'action_view/template' require 'action_view/template'
require 'action_view/partial_template'
require 'action_view/inline_template' require 'action_view/inline_template'
require 'action_view/paths'
require 'action_view/base' require 'action_view/base'
require 'action_view/partials' require 'action_view/partials'
require 'action_view/template_error' require 'action_view/template_error'
I18n.load_path << "#{File.dirname(__FILE__)}/action_view/locale/en-US.yml"
require 'action_view/helpers'
ActionView::Base.class_eval do ActionView::Base.class_eval do
include ActionView::Partials include ActionView::Partials
include ActionView::Helpers
ActionView::Base.helper_modules.each do |helper_module|
include helper_module
end
end end

View file

@ -3,6 +3,12 @@ module ActionView #:nodoc:
end end
class MissingTemplate < ActionViewError #:nodoc: class MissingTemplate < ActionViewError #:nodoc:
def initialize(paths, path, template_format = nil)
full_template_path = path.include?('.') ? path : "#{path}.erb"
display_paths = paths.join(':')
template_type = (path =~ /layouts/i) ? 'layout' : 'template'
super("Missing #{template_type} #{full_template_path} in view path #{display_paths}")
end
end end
# Action View templates can be written in three ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERb # Action View templates can be written in three ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERb
@ -152,26 +158,29 @@ module ActionView #:nodoc:
# See the ActionView::Helpers::PrototypeHelper::GeneratorMethods documentation for more details. # See the ActionView::Helpers::PrototypeHelper::GeneratorMethods documentation for more details.
class Base class Base
include ERB::Util include ERB::Util
extend ActiveSupport::Memoizable
attr_reader :finder attr_accessor :base_path, :assigns, :template_extension
attr_accessor :base_path, :assigns, :template_extension, :first_render
attr_accessor :controller attr_accessor :controller
attr_writer :template_format attr_writer :template_format
attr_accessor :current_render_extension
# Specify trim mode for the ERB compiler. Defaults to '-'. attr_accessor :output_buffer
# See ERb documentation for suitable values.
@@erb_trim_mode = '-'
cattr_accessor :erb_trim_mode
# Specify whether file modification times should be checked to see if a template needs recompilation class << self
@@cache_template_loading = false delegate :erb_trim_mode=, :to => 'ActionView::TemplateHandlers::ERB'
cattr_accessor :cache_template_loading delegate :logger, :to => 'ActionController::Base'
end
def self.cache_template_extensions=(*args) # Templates that are exempt from layouts
ActiveSupport::Deprecation.warn("config.action_view.cache_template_extensions option has been deprecated and has no affect. " << @@exempt_from_layout = Set.new([/\.rjs$/])
"Please remove it from your config files.", caller)
# Don't render layouts for templates with the given extensions.
def self.exempt_from_layout(*extensions)
regexps = extensions.collect do |extension|
extension.is_a?(Regexp) ? extension : /\.#{Regexp.escape(extension.to_s)}$/
end
@@exempt_from_layout.merge(regexps)
end end
# Specify whether RJS responses should be wrapped in a try/catch block # Specify whether RJS responses should be wrapped in a try/catch block
@ -179,168 +188,182 @@ module ActionView #:nodoc:
@@debug_rjs = false @@debug_rjs = false
cattr_accessor :debug_rjs cattr_accessor :debug_rjs
@@erb_variable = '_erbout' # A warning will be displayed whenever an action results in a cache miss on your view paths.
cattr_accessor :erb_variable @@warn_cache_misses = false
class << self cattr_accessor :warn_cache_misses
deprecate :erb_variable= => 'The erb variable will no longer be configurable. Use the concat helper method instead of appending to it directly.'
end
attr_internal :request attr_internal :request
delegate :request_forgery_protection_token, :template, :params, :session, :cookies, :response, :headers, delegate :request_forgery_protection_token, :template, :params, :session, :cookies, :response, :headers,
:flash, :logger, :action_name, :to => :controller :flash, :logger, :action_name, :controller_name, :to => :controller
module CompiledTemplates #:nodoc: module CompiledTemplates #:nodoc:
# holds compiled template code # holds compiled template code
end end
include CompiledTemplates include CompiledTemplates
# Maps inline templates to their method names def self.process_view_paths(value)
cattr_accessor :method_names ActionView::PathSet.new(Array(value))
@@method_names = {}
# Map method names to the names passed in local assigns so far
@@template_args = {}
# Cache public asset paths
cattr_reader :computed_public_paths
@@computed_public_paths = {}
class ObjectWrapper < Struct.new(:value) #:nodoc:
end end
def self.helper_modules #:nodoc: attr_reader :helpers
helpers = []
Dir.entries(File.expand_path("#{File.dirname(__FILE__)}/helpers")).sort.each do |file| class ProxyModule < Module
next unless file =~ /^([a-z][a-z_]*_helper).rb$/ def initialize(receiver)
require "action_view/helpers/#{$1}" @receiver = receiver
helper_module_name = $1.camelize
if Helpers.const_defined?(helper_module_name)
helpers << Helpers.const_get(helper_module_name)
end end
def include(*args)
super(*args)
@receiver.extend(*args)
end end
return helpers
end end
def initialize(view_paths = [], assigns_for_first_render = {}, controller = nil)#:nodoc: def initialize(view_paths = [], assigns_for_first_render = {}, controller = nil)#:nodoc:
@assigns = assigns_for_first_render @assigns = assigns_for_first_render
@assigns_added = nil @assigns_added = nil
@controller = controller @controller = controller
@finder = TemplateFinder.new(self, view_paths) @helpers = ProxyModule.new(self)
self.view_paths = view_paths
end end
# Renders the template present at <tt>template_path</tt>. If <tt>use_full_path</tt> is set to true, attr_reader :view_paths
# it's relative to the view_paths array, 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 = {}) #:nodoc:
if defined?(ActionMailer) && defined?(ActionMailer::Base) && controller.is_a?(ActionMailer::Base) && !template_path.include?("/")
raise ActionViewError, <<-END_ERROR
Due to changes in ActionMailer, you need to provide the mailer_name along with the template name.
render "user_mailer/signup" def view_paths=(paths)
render :file => "user_mailer/signup" @view_paths = self.class.process_view_paths(paths)
If you are rendering a subtemplate, you must now use controller-like partial syntax:
render :partial => 'signup' # no mailer_name necessary
END_ERROR
end
Template.new(self, template_path, use_full_path, local_assigns).render_template
end end
# Renders the template present at <tt>template_path</tt> (relative to the view_paths array). # Renders the template present at <tt>template_path</tt> (relative to the view_paths array).
# The hash in <tt>local_assigns</tt> is made available as local variables. # The hash in <tt>local_assigns</tt> is made available as local variables.
def render(options = {}, local_assigns = {}, &block) #:nodoc: def render(options = {}, local_assigns = {}, &block) #:nodoc:
local_assigns ||= {}
if options.is_a?(String) if options.is_a?(String)
render_file(options, true, local_assigns) render(:file => options, :locals => local_assigns)
elsif options == :update elsif options == :update
update_page(&block) update_page(&block)
elsif options.is_a?(Hash) elsif options.is_a?(Hash)
use_full_path = options[:use_full_path] options = options.reverse_merge(:locals => {})
options = options.reverse_merge(:locals => {}, :use_full_path => true) if options[:layout]
_render_with_layout(options, local_assigns, &block)
if partial_layout = options.delete(:layout)
if block_given?
wrap_content_for_layout capture(&block) do
concat(render(options.merge(:partial => partial_layout)), block.binding)
end
else
wrap_content_for_layout render(options) do
render(options.merge(:partial => partial_layout))
end
end
elsif options[:file] elsif options[:file]
render_file(options[:file], use_full_path || false, options[:locals]) _pick_template(options[:file]).render_template(self, options[:locals])
elsif options[:partial] && options[:collection]
render_partial_collection(options[:partial], options[:collection], options[:spacer_template], options[:locals])
elsif options[:partial] elsif options[:partial]
render_partial(options[:partial], ActionView::Base::ObjectWrapper.new(options[:object]), options[:locals]) render_partial(options)
elsif options[:inline] elsif options[:inline]
template = InlineTemplate.new(self, options[:inline], options[:locals], options[:type]) InlineTemplate.new(options[:inline], options[:type]).render(self, options[:locals])
render_template(template) elsif options[:text]
options[:text]
end end
end end
end end
def render_template(template) #:nodoc: # The format to be used when choosing between multiple templates with
template.render_template # the same name but differing formats. See +Request#template_format+
end # for more details.
# Returns true is the file may be rendered implicitly.
def file_public?(template_path)#:nodoc:
template_path.split('/').last[0,1] != '_'
end
# Returns a symbolized version of the <tt>:format</tt> parameter of the request,
# or <tt>:html</tt> by default.
#
# EXCEPTION: If the <tt>:format</tt> parameter is not set, the Accept header will be examined for
# whether it contains the JavaScript mime type as its first priority. If that's the case,
# it will be used. This ensures that Ajax applications can use the same URL to support both
# JavaScript and non-JavaScript users.
def template_format def template_format
return @template_format if @template_format if defined? @template_format
@template_format
if controller && controller.respond_to?(:request) elsif controller && controller.respond_to?(:request)
parameter_format = controller.request.parameters[:format] @template_format = controller.request.template_format
accept_format = controller.request.accepts.first
case
when parameter_format.blank? && accept_format != :js
@template_format = :html
when parameter_format.blank? && accept_format == :js
@template_format = :js
else
@template_format = parameter_format.to_sym
end
else else
@template_format = :html @template_format = :html
end end
end end
private private
def wrap_content_for_layout(content) attr_accessor :_first_render, :_last_render
original_content_for_layout = @content_for_layout
@content_for_layout = content
returning(yield) { @content_for_layout = original_content_for_layout }
end
# Evaluate the local assigns and pushes them to the view. # Evaluates the local assigns and controller ivars, pushes them to the view.
def evaluate_assigns def _evaluate_assigns_and_ivars #:nodoc:
unless @assigns_added unless @assigns_added
assign_variables_from_controller @assigns.each { |key, value| instance_variable_set("@#{key}", value) }
_copy_ivars_from_controller
@assigns_added = true @assigns_added = true
end end
end end
# Assigns instance variables from the controller to the view. def _copy_ivars_from_controller #:nodoc:
def assign_variables_from_controller if @controller
@assigns.each { |key, value| instance_variable_set("@#{key}", value) } variables = @controller.instance_variable_names
variables -= @controller.protected_instance_variables if @controller.respond_to?(:protected_instance_variables)
variables.each { |name| instance_variable_set(name, @controller.instance_variable_get(name)) }
end
end end
def execute(template) def _set_controller_content_type(content_type) #:nodoc:
send(template.method, template.locals) do |*names| if controller.respond_to?(:response)
instance_variable_get "@content_for_#{names.first || 'layout'}" controller.response.content_type ||= content_type
end
end
def _pick_template(template_path)
return template_path if template_path.respond_to?(:render)
path = template_path.sub(/^\//, '')
if m = path.match(/(.*)\.(\w+)$/)
template_file_name, template_file_extension = m[1], m[2]
else
template_file_name = path
end
# OPTIMIZE: Checks to lookup template in view path
if template = self.view_paths["#{template_file_name}.#{template_format}"]
template
elsif template = self.view_paths[template_file_name]
template
elsif _first_render && template = self.view_paths["#{template_file_name}.#{_first_render.format_and_extension}"]
template
elsif template_format == :js && template = self.view_paths["#{template_file_name}.html"]
@template_format = :html
template
else
template = Template.new(template_path, view_paths)
if self.class.warn_cache_misses && logger
logger.debug "[PERFORMANCE] Rendering a template that was " +
"not found in view path. Templates outside the view path are " +
"not cached and result in expensive disk operations. Move this " +
"file into #{view_paths.join(':')} or add the folder to your " +
"view path list"
end
template
end
end
memoize :_pick_template
def _exempt_from_layout?(template_path) #:nodoc:
template = _pick_template(template_path).to_s
@@exempt_from_layout.any? { |ext| template =~ ext }
rescue ActionView::MissingTemplate
return false
end
def _render_with_layout(options, local_assigns, &block) #:nodoc:
partial_layout = options.delete(:layout)
if block_given?
begin
@_proc_for_layout = block
concat(render(options.merge(:partial => partial_layout)))
ensure
@_proc_for_layout = nil
end
else
begin
original_content_for_layout = @content_for_layout if defined?(@content_for_layout)
@content_for_layout = render(options)
if (options[:inline] || options[:file] || options[:text])
@cached_content_for_layout = @content_for_layout
render(:file => partial_layout, :locals => local_assigns)
else
render(options.merge(:partial => partial_layout))
end
ensure
@content_for_layout = original_content_for_layout
end
end end
end end
end end

View file

@ -0,0 +1,38 @@
Dir.entries(File.expand_path("#{File.dirname(__FILE__)}/helpers")).sort.each do |file|
next unless file =~ /^([a-z][a-z_]*_helper).rb$/
require "action_view/helpers/#{$1}"
end
module ActionView #:nodoc:
module Helpers #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
include SanitizeHelper::ClassMethods
end
include ActiveRecordHelper
include AssetTagHelper
include AtomFeedHelper
include BenchmarkHelper
include CacheHelper
include CaptureHelper
include DateHelper
include DebugHelper
include FormHelper
include FormOptionsHelper
include FormTagHelper
include NumberHelper
include PrototypeHelper
include RecordIdentificationHelper
include RecordTagHelper
include SanitizeHelper
include ScriptaculousHelper
include TagHelper
include TextHelper
include TranslationHelper
include UrlHelper
end
end

View file

@ -90,22 +90,40 @@ module ActionView
end end
# Returns a string containing the error message attached to the +method+ on the +object+ if one exists. # Returns a string containing the error message attached to the +method+ on the +object+ if one exists.
# This error message is wrapped in a <tt>DIV</tt> tag, which can be extended to include a +prepend_text+ and/or +append_text+ # This error message is wrapped in a <tt>DIV</tt> tag, which can be extended to include a <tt>:prepend_text</tt>
# (to properly explain the error), and a +css_class+ to style it accordingly. +object+ should either be the name of an instance variable or # and/or <tt>:append_text</tt> (to properly explain the error), and a <tt>:css_class</tt> to style it
# the actual object. As an example, let's say you have a model <tt>@post</tt> that has an error message on the +title+ attribute: # accordingly. +object+ should either be the name of an instance variable or the actual object. The method can be
# passed in either as a string or a symbol.
# As an example, let's say you have a model <tt>@post</tt> that has an error message on the +title+ attribute:
# #
# <%= error_message_on "post", "title" %> # <%= error_message_on "post", "title" %>
# # => <div class="formError">can't be empty</div> # # => <div class="formError">can't be empty</div>
# #
# <%= error_message_on @post, "title" %> # <%= error_message_on @post, :title %>
# # => <div class="formError">can't be empty</div> # # => <div class="formError">can't be empty</div>
# #
# <%= error_message_on "post", "title", "Title simply ", " (or it won't work).", "inputError" %> # <%= error_message_on "post", "title",
# # => <div class="inputError">Title simply can't be empty (or it won't work).</div> # :prepend_text => "Title simply ",
def error_message_on(object, method, prepend_text = "", append_text = "", css_class = "formError") # :append_text => " (or it won't work).",
# :css_class => "inputError" %>
def error_message_on(object, method, *args)
options = args.extract_options!
unless args.empty?
ActiveSupport::Deprecation.warn('error_message_on takes an option hash instead of separate ' +
'prepend_text, append_text, and css_class arguments', caller)
options[:prepend_text] = args[0] || ''
options[:append_text] = args[1] || ''
options[:css_class] = args[2] || 'formError'
end
options.reverse_merge!(:prepend_text => '', :append_text => '', :css_class => 'formError')
if (obj = (object.respond_to?(:errors) ? object : instance_variable_get("@#{object}"))) && if (obj = (object.respond_to?(:errors) ? object : instance_variable_get("@#{object}"))) &&
(errors = obj.errors.on(method)) (errors = obj.errors.on(method))
content_tag("div", "#{prepend_text}#{errors.is_a?(Array) ? errors.first : errors}#{append_text}", :class => css_class) content_tag("div",
"#{options[:prepend_text]}#{errors.is_a?(Array) ? errors.first : errors}#{options[:append_text]}",
:class => options[:css_class]
)
else else
'' ''
end end
@ -141,7 +159,7 @@ module ActionView
# #
# error_messages_for 'user_common', 'user', :object_name => 'user' # error_messages_for 'user_common', 'user', :object_name => 'user'
# #
# If the objects cannot be located as instance variables, you can add an extra <tt>:object</tt> paremeter which gives the actual # If the objects cannot be located as instance variables, you can add an extra <tt>:object</tt> parameter which gives the actual
# object (or array of objects to use): # object (or array of objects to use):
# #
# error_messages_for 'user', :object => @question.user # error_messages_for 'user', :object => @question.user
@ -151,11 +169,13 @@ module ActionView
# instance yourself and set it up. View the source of this method to see how easy it is. # instance yourself and set it up. View the source of this method to see how easy it is.
def error_messages_for(*params) def error_messages_for(*params)
options = params.extract_options!.symbolize_keys options = params.extract_options!.symbolize_keys
if object = options.delete(:object) if object = options.delete(:object)
objects = [object].flatten objects = [object].flatten
else else
objects = params.collect {|object_name| instance_variable_get("@#{object_name}") }.compact objects = params.collect {|object_name| instance_variable_get("@#{object_name}") }.compact
end end
count = objects.inject(0) {|sum, object| sum + object.errors.count } count = objects.inject(0) {|sum, object| sum + object.errors.count }
unless count.zero? unless count.zero?
html = {} html = {}
@ -168,16 +188,25 @@ module ActionView
end end
end end
options[:object_name] ||= params.first options[:object_name] ||= params.first
options[:header_message] = "#{pluralize(count, 'error')} prohibited this #{options[:object_name].to_s.gsub('_', ' ')} from being saved" unless options.include?(:header_message)
options[:message] ||= 'There were problems with the following fields:' unless options.include?(:message) I18n.with_options :locale => options[:locale], :scope => [:activerecord, :errors, :template] do |locale|
header_message = if options.include?(:header_message)
options[:header_message]
else
object_name = options[:object_name].to_s.gsub('_', ' ')
object_name = I18n.t(object_name, :default => object_name, :scope => [:activerecord, :models], :count => 1)
locale.t :header, :count => count, :model => object_name
end
message = options.include?(:message) ? options[:message] : locale.t(:body)
error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }.join error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }.join
contents = '' contents = ''
contents << content_tag(options[:header_tag] || :h2, options[:header_message]) unless options[:header_message].blank? contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank?
contents << content_tag(:p, options[:message]) unless options[:message].blank? contents << content_tag(:p, message) unless message.blank?
contents << content_tag(:ul, error_messages) contents << content_tag(:ul, error_messages)
content_tag(:div, contents, html) content_tag(:div, contents, html)
end
else else
'' ''
end end
@ -217,7 +246,7 @@ module ActionView
alias_method :tag_without_error_wrapping, :tag alias_method :tag_without_error_wrapping, :tag
def tag(name, options) def tag(name, options)
if object.respond_to?("errors") && object.errors.respond_to?("on") if object.respond_to?(:errors) && object.errors.respond_to?(:on)
error_wrapping(tag_without_error_wrapping(name, options), object.errors.on(@method_name)) error_wrapping(tag_without_error_wrapping(name, options), object.errors.on(@method_name))
else else
tag_without_error_wrapping(name, options) tag_without_error_wrapping(name, options)
@ -226,7 +255,7 @@ module ActionView
alias_method :content_tag_without_error_wrapping, :content_tag alias_method :content_tag_without_error_wrapping, :content_tag
def content_tag(name, value, options) def content_tag(name, value, options)
if object.respond_to?("errors") && object.errors.respond_to?("on") 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)) error_wrapping(content_tag_without_error_wrapping(name, value, options), object.errors.on(@method_name))
else else
content_tag_without_error_wrapping(name, value, options) content_tag_without_error_wrapping(name, value, options)
@ -235,7 +264,7 @@ module ActionView
alias_method :to_date_select_tag_without_error_wrapping, :to_date_select_tag alias_method :to_date_select_tag_without_error_wrapping, :to_date_select_tag
def to_date_select_tag(options = {}, html_options = {}) def to_date_select_tag(options = {}, html_options = {})
if object.respond_to?("errors") && object.errors.respond_to?("on") if object.respond_to?(:errors) && object.errors.respond_to?(:on)
error_wrapping(to_date_select_tag_without_error_wrapping(options, html_options), object.errors.on(@method_name)) error_wrapping(to_date_select_tag_without_error_wrapping(options, html_options), object.errors.on(@method_name))
else else
to_date_select_tag_without_error_wrapping(options, html_options) to_date_select_tag_without_error_wrapping(options, html_options)
@ -244,7 +273,7 @@ module ActionView
alias_method :to_datetime_select_tag_without_error_wrapping, :to_datetime_select_tag alias_method :to_datetime_select_tag_without_error_wrapping, :to_datetime_select_tag
def to_datetime_select_tag(options = {}, html_options = {}) def to_datetime_select_tag(options = {}, html_options = {})
if object.respond_to?("errors") && object.errors.respond_to?("on") if object.respond_to?(:errors) && object.errors.respond_to?(:on)
error_wrapping(to_datetime_select_tag_without_error_wrapping(options, html_options), object.errors.on(@method_name)) error_wrapping(to_datetime_select_tag_without_error_wrapping(options, html_options), object.errors.on(@method_name))
else else
to_datetime_select_tag_without_error_wrapping(options, html_options) to_datetime_select_tag_without_error_wrapping(options, html_options)
@ -253,7 +282,7 @@ module ActionView
alias_method :to_time_select_tag_without_error_wrapping, :to_time_select_tag alias_method :to_time_select_tag_without_error_wrapping, :to_time_select_tag
def to_time_select_tag(options = {}, html_options = {}) def to_time_select_tag(options = {}, html_options = {})
if object.respond_to?("errors") && object.errors.respond_to?("on") if object.respond_to?(:errors) && object.errors.respond_to?(:on)
error_wrapping(to_time_select_tag_without_error_wrapping(options, html_options), object.errors.on(@method_name)) error_wrapping(to_time_select_tag_without_error_wrapping(options, html_options), object.errors.on(@method_name))
else else
to_time_select_tag_without_error_wrapping(options, html_options) to_time_select_tag_without_error_wrapping(options, html_options)
@ -269,7 +298,7 @@ module ActionView
end end
def column_type def column_type
object.send("column_for_attribute", @method_name).type object.send(:column_for_attribute, @method_name).type
end end
end end
end end

View file

@ -104,6 +104,7 @@ module ActionView
ASSETS_DIR = defined?(Rails.public_path) ? Rails.public_path : "public" ASSETS_DIR = defined?(Rails.public_path) ? Rails.public_path : "public"
JAVASCRIPTS_DIR = "#{ASSETS_DIR}/javascripts" JAVASCRIPTS_DIR = "#{ASSETS_DIR}/javascripts"
STYLESHEETS_DIR = "#{ASSETS_DIR}/stylesheets" STYLESHEETS_DIR = "#{ASSETS_DIR}/stylesheets"
JAVASCRIPT_DEFAULT_SOURCES = ['prototype', 'effects', 'dragdrop', 'controls'].freeze unless const_defined?(:JAVASCRIPT_DEFAULT_SOURCES)
# Returns a link tag that browsers and news readers can use to auto-detect # Returns a link tag that browsers and news readers can use to auto-detect
# an RSS or ATOM feed. The +type+ can either be <tt>:rss</tt> (default) or # an RSS or ATOM feed. The +type+ can either be <tt>:rss</tt> (default) or
@ -150,14 +151,10 @@ module ActionView
# javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr.js # javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr.js
# javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js # javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js
def javascript_path(source) def javascript_path(source)
compute_public_path(source, 'javascripts', 'js') JavaScriptTag.create(self, @controller, source).public_path
end end
alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route
JAVASCRIPT_DEFAULT_SOURCES = ['prototype', 'effects', 'dragdrop', 'controls'] unless const_defined?(:JAVASCRIPT_DEFAULT_SOURCES)
@@javascript_expansions = { :defaults => JAVASCRIPT_DEFAULT_SOURCES.dup }
@@stylesheet_expansions = {}
# Returns an html script tag for each of the +sources+ provided. You # Returns an html script tag for each of the +sources+ provided. You
# can pass in the filename (.js extension is optional) of javascript files # can pass in the filename (.js extension is optional) of javascript files
# that exist in your public/javascripts directory for inclusion into the # that exist in your public/javascripts directory for inclusion into the
@ -209,6 +206,10 @@ module ActionView
# Note that the default javascript files will be included first. So Prototype and Scriptaculous are available to # Note that the default javascript files will be included first. So Prototype and Scriptaculous are available to
# all subsequently included files. # all subsequently included files.
# #
# If you want Rails to search in all the subdirectories under javascripts, you should explicitly set <tt>:recursive</tt>:
#
# javascript_include_tag :all, :recursive => true
#
# == Caching multiple javascripts into one # == Caching multiple javascripts into one
# #
# You can also cache multiple javascripts into one file, which requires less HTTP connections to download and can better be # You can also cache multiple javascripts into one file, which requires less HTTP connections to download and can better be
@ -235,18 +236,27 @@ module ActionView
# #
# javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when ActionController::Base.perform_caching is true => # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when ActionController::Base.perform_caching is true =>
# <script type="text/javascript" src="/javascripts/shop.js"></script> # <script type="text/javascript" src="/javascripts/shop.js"></script>
#
# The <tt>:recursive</tt> option is also available for caching:
#
# javascript_include_tag :all, :cache => true, :recursive => true
def javascript_include_tag(*sources) def javascript_include_tag(*sources)
options = sources.extract_options!.stringify_keys options = sources.extract_options!.stringify_keys
cache = options.delete("cache") cache = options.delete("cache")
recursive = options.delete("recursive")
if ActionController::Base.perform_caching && cache if ActionController::Base.perform_caching && cache
joined_javascript_name = (cache == true ? "all" : cache) + ".js" joined_javascript_name = (cache == true ? "all" : cache) + ".js"
joined_javascript_path = File.join(JAVASCRIPTS_DIR, joined_javascript_name) joined_javascript_path = File.join(JAVASCRIPTS_DIR, joined_javascript_name)
write_asset_file_contents(joined_javascript_path, compute_javascript_paths(sources)) unless File.exists?(joined_javascript_path)
JavaScriptSources.create(self, @controller, sources, recursive).write_asset_file_contents(joined_javascript_path)
end
javascript_src_tag(joined_javascript_name, options) javascript_src_tag(joined_javascript_name, options)
else else
expand_javascript_sources(sources).collect { |source| javascript_src_tag(source, options) }.join("\n") JavaScriptSources.create(self, @controller, sources, recursive).expand_sources.collect { |source|
javascript_src_tag(source, options)
}.join("\n")
end end
end end
@ -262,7 +272,7 @@ module ActionView
# <script type="text/javascript" src="/javascripts/body.js"></script> # <script type="text/javascript" src="/javascripts/body.js"></script>
# <script type="text/javascript" src="/javascripts/tail.js"></script> # <script type="text/javascript" src="/javascripts/tail.js"></script>
def self.register_javascript_expansion(expansions) def self.register_javascript_expansion(expansions)
@@javascript_expansions.merge!(expansions) JavaScriptSources.expansions.merge!(expansions)
end end
# Register one or more stylesheet files to be included when <tt>symbol</tt> # Register one or more stylesheet files to be included when <tt>symbol</tt>
@ -277,7 +287,7 @@ module ActionView
# <link href="/stylesheets/body.css" media="screen" rel="stylesheet" type="text/css" /> # <link href="/stylesheets/body.css" media="screen" rel="stylesheet" type="text/css" />
# <link href="/stylesheets/tail.css" media="screen" rel="stylesheet" type="text/css" /> # <link href="/stylesheets/tail.css" media="screen" rel="stylesheet" type="text/css" />
def self.register_stylesheet_expansion(expansions) def self.register_stylesheet_expansion(expansions)
@@stylesheet_expansions.merge!(expansions) StylesheetSources.expansions.merge!(expansions)
end end
# Register one or more additional JavaScript files to be included when # Register one or more additional JavaScript files to be included when
@ -285,11 +295,11 @@ module ActionView
# typically intended to be called from plugin initialization to register additional # typically intended to be called from plugin initialization to register additional
# .js files that the plugin installed in <tt>public/javascripts</tt>. # .js files that the plugin installed in <tt>public/javascripts</tt>.
def self.register_javascript_include_default(*sources) def self.register_javascript_include_default(*sources)
@@javascript_expansions[:defaults].concat(sources) JavaScriptSources.expansions[:defaults].concat(sources)
end end
def self.reset_javascript_include_default #:nodoc: def self.reset_javascript_include_default #:nodoc:
@@javascript_expansions[:defaults] = JAVASCRIPT_DEFAULT_SOURCES.dup JavaScriptSources.expansions[:defaults] = JAVASCRIPT_DEFAULT_SOURCES.dup
end end
# Computes the path to a stylesheet asset in the public stylesheets directory. # Computes the path to a stylesheet asset in the public stylesheets directory.
@ -304,7 +314,7 @@ module ActionView
# stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style.css # stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style.css
# stylesheet_path "http://www.railsapplication.com/css/style.js" # => http://www.railsapplication.com/css/style.css # stylesheet_path "http://www.railsapplication.com/css/style.js" # => http://www.railsapplication.com/css/style.css
def stylesheet_path(source) def stylesheet_path(source)
compute_public_path(source, 'stylesheets', 'css') StylesheetTag.create(self, @controller, source).public_path
end end
alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route
@ -332,13 +342,17 @@ module ActionView
# <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" type="text/css" /> # <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" type="text/css" />
# <link href="/css/stylish.css" media="screen" rel="stylesheet" type="text/css" /> # <link href="/css/stylish.css" media="screen" rel="stylesheet" type="text/css" />
# #
# You can also include all styles in the stylesheet directory using <tt>:all</tt> as the source: # You can also include all styles in the stylesheets directory using <tt>:all</tt> as the source:
# #
# stylesheet_link_tag :all # => # stylesheet_link_tag :all # =>
# <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" /> # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" />
# <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" /> # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" />
# <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" /> # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" />
# #
# If you want Rails to search in all the subdirectories under stylesheets, you should explicitly set <tt>:recursive</tt>:
#
# stylesheet_link_tag :all, :recursive => true
#
# == Caching multiple stylesheets into one # == Caching multiple stylesheets into one
# #
# You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be # You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be
@ -362,18 +376,27 @@ module ActionView
# #
# stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when ActionController::Base.perform_caching is true => # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when ActionController::Base.perform_caching is true =>
# <link href="/stylesheets/payment.css" media="screen" rel="stylesheet" type="text/css" /> # <link href="/stylesheets/payment.css" media="screen" rel="stylesheet" type="text/css" />
#
# The <tt>:recursive</tt> option is also available for caching:
#
# stylesheet_link_tag :all, :cache => true, :recursive => true
def stylesheet_link_tag(*sources) def stylesheet_link_tag(*sources)
options = sources.extract_options!.stringify_keys options = sources.extract_options!.stringify_keys
cache = options.delete("cache") cache = options.delete("cache")
recursive = options.delete("recursive")
if ActionController::Base.perform_caching && cache if ActionController::Base.perform_caching && cache
joined_stylesheet_name = (cache == true ? "all" : cache) + ".css" joined_stylesheet_name = (cache == true ? "all" : cache) + ".css"
joined_stylesheet_path = File.join(STYLESHEETS_DIR, joined_stylesheet_name) joined_stylesheet_path = File.join(STYLESHEETS_DIR, joined_stylesheet_name)
write_asset_file_contents(joined_stylesheet_path, compute_stylesheet_paths(sources)) unless File.exists?(joined_stylesheet_path)
StylesheetSources.create(self, @controller, sources, recursive).write_asset_file_contents(joined_stylesheet_path)
end
stylesheet_tag(joined_stylesheet_name, options) stylesheet_tag(joined_stylesheet_name, options)
else else
expand_stylesheet_sources(sources).collect { |source| stylesheet_tag(source, options) }.join("\n") StylesheetSources.create(self, @controller, sources, recursive).expand_sources.collect { |source|
stylesheet_tag(source, options)
}.join("\n")
end end
end end
@ -388,7 +411,7 @@ module ActionView
# image_path("/icons/edit.png") # => /icons/edit.png # image_path("/icons/edit.png") # => /icons/edit.png
# image_path("http://www.railsapplication.com/img/edit.png") # => http://www.railsapplication.com/img/edit.png # image_path("http://www.railsapplication.com/img/edit.png") # => http://www.railsapplication.com/img/edit.png
def image_path(source) def image_path(source)
compute_public_path(source, 'images') ImageTag.create(self, @controller, source).public_path
end end
alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route
@ -444,61 +467,153 @@ module ActionView
end end
private private
def file_exist?(path) def javascript_src_tag(source, options)
@@file_exist_cache ||= {} content_tag("script", "", { "type" => Mime::JS, "src" => path_to_javascript(source) }.merge(options))
if !(@@file_exist_cache[path] ||= File.exist?(path))
@@file_exist_cache[path] = true
false
else
true
end end
def stylesheet_tag(source, options)
tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => html_escape(path_to_stylesheet(source)) }.merge(options), false, false)
end
module ImageAsset
DIRECTORY = 'images'.freeze
def directory
DIRECTORY
end
def extension
nil
end
end
module JavaScriptAsset
DIRECTORY = 'javascripts'.freeze
EXTENSION = 'js'.freeze
def public_directory
JAVASCRIPTS_DIR
end
def directory
DIRECTORY
end
def extension
EXTENSION
end
end
module StylesheetAsset
DIRECTORY = 'stylesheets'.freeze
EXTENSION = 'css'.freeze
def public_directory
STYLESHEETS_DIR
end
def directory
DIRECTORY
end
def extension
EXTENSION
end
end
class AssetTag
extend ActiveSupport::Memoizable
Cache = {}
CacheGuard = Mutex.new
def self.create(template, controller, source, include_host = true)
CacheGuard.synchronize do
key = if controller.respond_to?(:request)
[self, controller.request.protocol,
ActionController::Base.asset_host,
ActionController::Base.relative_url_root,
source, include_host]
else
[self, ActionController::Base.asset_host, source, include_host]
end
Cache[key] ||= new(template, controller, source, include_host).freeze
end
end
ProtocolRegexp = %r{^[-a-z]+://}.freeze
def initialize(template, controller, source, include_host = true)
# NOTE: The template arg is temporarily needed for a legacy plugin
# hook that is expected to call rewrite_asset_path on the
# template. This should eventually be removed.
@template = template
@controller = controller
@source = source
@include_host = include_host
end
def public_path
compute_public_path(@source)
end
memoize :public_path
def asset_file_path
File.join(ASSETS_DIR, public_path.split('?').first)
end
memoize :asset_file_path
def contents
File.read(asset_file_path)
end
def mtime
File.mtime(asset_file_path)
end
private
def request
@controller.request
end
def request?
@controller.respond_to?(:request)
end end
# Add the the extension +ext+ if not present. Return full URLs otherwise untouched. # Add the the extension +ext+ if not present. Return full URLs otherwise untouched.
# Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL # Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL
# roots. Rewrite the asset path for cache-busting asset ids. Include # roots. Rewrite the asset path for cache-busting asset ids. Include
# asset host, if configured, with the correct request protocol. # asset host, if configured, with the correct request protocol.
def compute_public_path(source, dir, ext = nil, include_host = true) def compute_public_path(source)
has_request = @controller.respond_to?(:request) source += ".#{extension}" if missing_extension?(source)
unless source =~ ProtocolRegexp
cache_key = source = "/#{directory}/#{source}" unless source[0] == ?/
if has_request source = prepend_relative_url_root(source)
[ @controller.request.protocol, source = rewrite_asset_path(source)
ActionController::Base.asset_host.to_s,
@controller.request.relative_url_root,
dir, source, ext, include_host ].join
else
[ ActionController::Base.asset_host.to_s,
dir, source, ext, include_host ].join
end end
source = prepend_asset_host(source)
ActionView::Base.computed_public_paths[cache_key] ||=
begin
source += ".#{ext}" if ext && File.extname(source).blank? || File.exist?(File.join(ASSETS_DIR, dir, "#{source}.#{ext}"))
if source =~ %r{^[-a-z]+://}
source source
end
def missing_extension?(source)
extension && (File.extname(source).blank? || File.exist?(File.join(ASSETS_DIR, directory, "#{source}.#{extension}")))
end
def prepend_relative_url_root(source)
relative_url_root = ActionController::Base.relative_url_root
if request? && @include_host && source !~ %r{^#{relative_url_root}/}
"#{relative_url_root}#{source}"
else else
source = "/#{dir}/#{source}" unless source[0] == ?/ source
if has_request
unless source =~ %r{^#{@controller.request.relative_url_root}/}
source = "#{@controller.request.relative_url_root}#{source}"
end end
end end
rewrite_asset_path(source) def prepend_asset_host(source)
end if @include_host && source !~ ProtocolRegexp
end
source = ActionView::Base.computed_public_paths[cache_key]
if include_host && source !~ %r{^[-a-z]+://}
host = compute_asset_host(source) host = compute_asset_host(source)
if request? && !host.blank? && host !~ ProtocolRegexp
if has_request && !host.blank? && host !~ %r{^[-a-z]+://} host = "#{request.protocol}#{host}"
host = "#{@controller.request.protocol}#{host}"
end end
"#{host}#{source}" "#{host}#{source}"
else else
source source
@ -514,7 +629,7 @@ module ActionView
if host.is_a?(Proc) if host.is_a?(Proc)
case host.arity case host.arity
when 2 when 2
host.call(source, @controller.request) host.call(source, request)
else else
host.call(source) host.call(source)
end end
@ -543,53 +658,62 @@ module ActionView
# Break out the asset path rewrite in case plugins wish to put the asset id # Break out the asset path rewrite in case plugins wish to put the asset id
# someplace other than the query string. # someplace other than the query string.
def rewrite_asset_path(source) def rewrite_asset_path(source)
if @template.respond_to?(:rewrite_asset_path)
# DEPRECATE: This way to override rewrite_asset_path
@template.send(:rewrite_asset_path, source)
else
asset_id = rails_asset_id(source) asset_id = rails_asset_id(source)
if asset_id.blank? if asset_id.blank?
source source
else else
source + "?#{asset_id}" "#{source}?#{asset_id}"
end
end
end end
end end
def javascript_src_tag(source, options) class ImageTag < AssetTag
content_tag("script", "", { "type" => Mime::JS, "src" => path_to_javascript(source) }.merge(options)) include ImageAsset
end end
def stylesheet_tag(source, options) class JavaScriptTag < AssetTag
tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => html_escape(path_to_stylesheet(source)) }.merge(options), false, false) include JavaScriptAsset
end end
def compute_javascript_paths(sources) class StylesheetTag < AssetTag
expand_javascript_sources(sources).collect { |source| compute_public_path(source, 'javascripts', 'js', false) } include StylesheetAsset
end end
def compute_stylesheet_paths(sources) class AssetCollection
expand_stylesheet_sources(sources).collect { |source| compute_public_path(source, 'stylesheets', 'css', false) } extend ActiveSupport::Memoizable
end
def expand_javascript_sources(sources) Cache = {}
if sources.include?(:all) CacheGuard = Mutex.new
all_javascript_files = Dir[File.join(JAVASCRIPTS_DIR, '*.js')].collect { |file| File.basename(file).gsub(/\.\w+$/, '') }.sort
@@all_javascript_sources ||= ((determine_source(:defaults, @@javascript_expansions).dup & all_javascript_files) + all_javascript_files).uniq def self.create(template, controller, sources, recursive)
else CacheGuard.synchronize do
expanded_sources = sources.collect do |source| key = [self, sources, recursive]
determine_source(source, @@javascript_expansions) Cache[key] ||= new(template, controller, sources, recursive).freeze
end.flatten
expanded_sources << "application" if sources.include?(:defaults) && file_exist?(File.join(JAVASCRIPTS_DIR, "application.js"))
expanded_sources
end end
end end
def expand_stylesheet_sources(sources) def initialize(template, controller, sources, recursive)
if sources.first == :all # NOTE: The template arg is temporarily needed for a legacy plugin
@@all_stylesheet_sources ||= Dir[File.join(STYLESHEETS_DIR, '*.css')].collect { |file| File.basename(file).gsub(/\.\w+$/, '') }.sort # hook. See NOTE under AssetTag#initialize for more details
else @template = template
sources.collect do |source| @controller = controller
determine_source(source, @@stylesheet_expansions) @sources = sources
end.flatten @recursive = recursive
end
end end
def write_asset_file_contents(joined_asset_path)
FileUtils.mkdir_p(File.dirname(joined_asset_path))
File.open(joined_asset_path, "w+") { |cache| cache.write(joined_contents) }
mt = latest_mtime
File.utime(mt, mt, joined_asset_path)
end
private
def determine_source(source, collection) def determine_source(source, collection)
case source case source
when Symbol when Symbol
@ -599,14 +723,87 @@ module ActionView
end end
end end
def join_asset_file_contents(paths) def validate_sources!
paths.collect { |path| File.read(File.join(ASSETS_DIR, path.split("?").first)) }.join("\n\n") @sources.collect { |source| determine_source(source, self.class.expansions) }.flatten
end end
def write_asset_file_contents(joined_asset_path, asset_paths) def all_asset_files
unless file_exist?(joined_asset_path) path = [public_directory, ('**' if @recursive), "*.#{extension}"].compact
FileUtils.mkdir_p(File.dirname(joined_asset_path)) Dir[File.join(*path)].collect { |file|
File.open(joined_asset_path, "w+") { |cache| cache.write(join_asset_file_contents(asset_paths)) } file[-(file.size - public_directory.size - 1)..-1].sub(/\.\w+$/, '')
}.sort
end
def tag_sources
expand_sources.collect { |source| tag_class.create(@template, @controller, source, false) }
end
def joined_contents
tag_sources.collect { |source| source.contents }.join("\n\n")
end
# Set mtime to the latest of the combined files to allow for
# consistent ETag without a shared filesystem.
def latest_mtime
tag_sources.map { |source| source.mtime }.max
end
end
class JavaScriptSources < AssetCollection
include JavaScriptAsset
EXPANSIONS = { :defaults => JAVASCRIPT_DEFAULT_SOURCES.dup }
def self.expansions
EXPANSIONS
end
APPLICATION_JS = "application".freeze
APPLICATION_FILE = "application.js".freeze
def expand_sources
if @sources.include?(:all)
assets = all_asset_files
((defaults.dup & assets) + assets).uniq!
else
expanded_sources = validate_sources!
expanded_sources << APPLICATION_JS if include_application?
expanded_sources
end
end
memoize :expand_sources
private
def tag_class
JavaScriptTag
end
def defaults
determine_source(:defaults, self.class.expansions)
end
def include_application?
@sources.include?(:defaults) && File.exist?(File.join(JAVASCRIPTS_DIR, APPLICATION_FILE))
end
end
class StylesheetSources < AssetCollection
include StylesheetAsset
EXPANSIONS = {}
def self.expansions
EXPANSIONS
end
def expand_sources
@sources.first == :all ? all_asset_files : validate_sources!
end
memoize :expand_sources
private
def tag_class
StylesheetTag
end end
end end
end end

View file

@ -47,9 +47,11 @@ module ActionView
# * <tt>:language</tt>: Defaults to "en-US". # * <tt>:language</tt>: Defaults to "en-US".
# * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host. # * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host.
# * <tt>:url</tt>: The URL for this feed. Defaults to the current URL. # * <tt>:url</tt>: The URL for this feed. Defaults to the current URL.
# * <tt>:id</tt>: The id for this feed. Defaults to "tag:#{request.host},#{options[:schema_date]}:#{request.request_uri.split(".")[0]}"
# * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you # * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you
# created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified, # created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified,
# 2005 is used (as an "I don't care" value). # 2005 is used (as an "I don't care" value).
# * <tt>:instruct</tt>: Hash of XML processing instructions in the form {target => {attribute => value, }} or {target => [{attribute => value, }, ]}
# #
# Other namespaces can be added to the root element: # Other namespaces can be added to the root element:
# #
@ -73,8 +75,20 @@ module ActionView
# end # end
# end # end
# #
# The Atom spec defines five elements (content rights title subtitle
# summary) which may directly contain xhtml content if :type => 'xhtml'
# is specified as an attribute. If so, this helper will take care of
# the enclosing div and xhtml namespace declaration. Example usage:
# #
# atom_feed yields an AtomFeedBuilder instance. # entry.summary :type => 'xhtml' do |xhtml|
# xhtml.p pluralize(order.line_items.count, "line item")
# xhtml.p "Shipped to #{order.address}"
# xhtml.p "Paid by #{order.pay_type}"
# end
#
#
# atom_feed yields an AtomFeedBuilder instance. Nested elements yield
# an AtomBuilder instance.
def atom_feed(options = {}, &block) def atom_feed(options = {}, &block)
if options[:schema_date] if options[:schema_date]
options[:schema_date] = options[:schema_date].strftime("%Y-%m-%d") if options[:schema_date].respond_to?(:strftime) options[:schema_date] = options[:schema_date].strftime("%Y-%m-%d") if options[:schema_date].respond_to?(:strftime)
@ -84,12 +98,21 @@ module ActionView
xml = options[:xml] || eval("xml", block.binding) xml = options[:xml] || eval("xml", block.binding)
xml.instruct! xml.instruct!
if options[:instruct]
options[:instruct].each do |target,attrs|
if attrs.respond_to?(:keys)
xml.instruct!(target, attrs)
elsif attrs.respond_to?(:each)
attrs.each { |attr_group| xml.instruct!(target, attr_group) }
end
end
end
feed_opts = {"xml:lang" => options[:language] || "en-US", "xmlns" => 'http://www.w3.org/2005/Atom'} feed_opts = {"xml:lang" => options[:language] || "en-US", "xmlns" => 'http://www.w3.org/2005/Atom'}
feed_opts.merge!(options).reject!{|k,v| !k.to_s.match(/^xml/)} feed_opts.merge!(options).reject!{|k,v| !k.to_s.match(/^xml/)}
xml.feed(feed_opts) do xml.feed(feed_opts) do
xml.id("tag:#{request.host},#{options[:schema_date]}:#{request.request_uri.split(".")[0]}") xml.id(options[:id] || "tag:#{request.host},#{options[:schema_date]}:#{request.request_uri.split(".")[0]}")
xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:root_url] || (request.protocol + request.host_with_port)) xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:root_url] || (request.protocol + request.host_with_port))
xml.link(:rel => 'self', :type => 'application/atom+xml', :href => options[:url] || request.url) xml.link(:rel => 'self', :type => 'application/atom+xml', :href => options[:url] || request.url)
@ -97,8 +120,38 @@ module ActionView
end end
end end
class AtomBuilder
def initialize(xml)
@xml = xml
end
class AtomFeedBuilder private
# Delegate to xml builder, first wrapping the element in a xhtml
# namespaced div element if the method and arguments indicate
# that an xhtml_block? is desired.
def method_missing(method, *arguments, &block)
if xhtml_block?(method, arguments)
@xml.__send__(method, *arguments) do
@xml.div(:xmlns => 'http://www.w3.org/1999/xhtml') do |xhtml|
block.call(xhtml)
end
end
else
@xml.__send__(method, *arguments, &block)
end
end
# True if the method name matches one of the five elements defined
# in the Atom spec as potentially containing XHTML content and
# if :type => 'xhtml' is, in fact, specified.
def xhtml_block?(method, arguments)
%w( content rights title subtitle summary ).include?(method.to_s) &&
arguments.last.respond_to?(:[]) &&
arguments.last[:type].to_s == 'xhtml'
end
end
class AtomFeedBuilder < AtomBuilder
def initialize(xml, view, feed_options = {}) def initialize(xml, view, feed_options = {})
@xml, @view, @feed_options = xml, view, feed_options @xml, @view, @feed_options = xml, view, feed_options
end end
@ -115,9 +168,10 @@ module ActionView
# * <tt>:published</tt>: Time first published. Defaults to the created_at attribute on the record if one such exists. # * <tt>:published</tt>: Time first published. Defaults to the created_at attribute on the record if one such exists.
# * <tt>:updated</tt>: Time of update. Defaults to the updated_at attribute on the record if one such exists. # * <tt>:updated</tt>: Time of update. Defaults to the updated_at attribute on the record if one such exists.
# * <tt>:url</tt>: The URL for this entry. Defaults to the polymorphic_url for the record. # * <tt>:url</tt>: The URL for this entry. Defaults to the polymorphic_url for the record.
# * <tt>:id</tt>: The ID for this entry. Defaults to "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}"
def entry(record, options = {}) def entry(record, options = {})
@xml.entry do @xml.entry do
@xml.id("tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}") @xml.id(options[:id] || "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}")
if options[:published] || (record.respond_to?(:created_at) && record.created_at) if options[:published] || (record.respond_to?(:created_at) && record.created_at)
@xml.published((options[:published] || record.created_at).xmlschema) @xml.published((options[:published] || record.created_at).xmlschema)
@ -129,15 +183,11 @@ module ActionView
@xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:url] || @view.polymorphic_url(record)) @xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:url] || @view.polymorphic_url(record))
yield @xml yield AtomBuilder.new(@xml)
end
end end
end end
private
def method_missing(method, *arguments, &block)
@xml.__send__(method, *arguments, &block)
end
end
end end
end end
end end

View file

@ -15,15 +15,15 @@ module ActionView
# <%= expensive_files_operation %> # <%= expensive_files_operation %>
# <% end %> # <% end %>
# #
# That would add something like "Process data files (0.34523)" to the log, # That would add something like "Process data files (345.2ms)" to the log,
# which you can then use to compare timings when optimizing your code. # which you can then use to compare timings when optimizing your code.
# #
# You may give an optional logger level as the second argument # You may give an optional logger level as the second argument
# (:debug, :info, :warn, :error); the default value is :info. # (:debug, :info, :warn, :error); the default value is :info.
def benchmark(message = "Benchmarking", level = :info) def benchmark(message = "Benchmarking", level = :info)
if controller.logger if controller.logger
real = Benchmark.realtime { yield } seconds = Benchmark.realtime { yield }
controller.logger.send(level, "#{message} (#{'%.5f' % real})") controller.logger.send(level, "#{message} (#{'%.1f' % (seconds * 1000)}ms)")
else else
yield yield
end end

View file

@ -32,8 +32,7 @@ module ActionView
# <i>Topics listed alphabetically</i> # <i>Topics listed alphabetically</i>
# <% end %> # <% end %>
def cache(name = {}, options = nil, &block) def cache(name = {}, options = nil, &block)
handler = Template.handler_class_for_extension(current_render_extension.to_sym) @controller.fragment_for(output_buffer, name, options, &block)
handler.new(@controller).cache_fragment(block, name, options)
end end
end end
end end

View file

@ -31,17 +31,12 @@ module ActionView
# </body></html> # </body></html>
# #
def capture(*args, &block) def capture(*args, &block)
# execute the block # Return captured buffer in erb.
begin if block_called_from_erb?(block)
buffer = eval(ActionView::Base.erb_variable, block.binding) with_output_buffer { block.call(*args) }
rescue
buffer = nil
end
if buffer.nil?
capture_block(*args, &block).to_s
else else
capture_erb_with_buffer(buffer, *args, &block).to_s # Return block result otherwise, but protect buffer also.
with_output_buffer { return block.call(*args) }
end end
end end
@ -121,40 +116,20 @@ module ActionView
# named <tt>@content_for_#{name_of_the_content_block}</tt>. The preferred usage is now # named <tt>@content_for_#{name_of_the_content_block}</tt>. The preferred usage is now
# <tt><%= yield :footer %></tt>. # <tt><%= yield :footer %></tt>.
def content_for(name, content = nil, &block) def content_for(name, content = nil, &block)
existing_content_for = instance_variable_get("@content_for_#{name}").to_s ivar = "@content_for_#{name}"
new_content_for = existing_content_for + (block_given? ? capture(&block) : content) content = capture(&block) if block_given?
instance_variable_set("@content_for_#{name}", new_content_for) instance_variable_set(ivar, "#{instance_variable_get(ivar)}#{content}")
nil
end end
private # Use an alternate output buffer for the duration of the block.
def capture_block(*args, &block) # Defaults to a new empty string.
block.call(*args) def with_output_buffer(buf = '') #:nodoc:
end self.output_buffer, old_buffer = buf, output_buffer
yield
def capture_erb(*args, &block) output_buffer
buffer = eval(ActionView::Base.erb_variable, block.binding) ensure
capture_erb_with_buffer(buffer, *args, &block) self.output_buffer = old_buffer
end
def capture_erb_with_buffer(buffer, *args, &block)
pos = buffer.length
block.call(*args)
# extract the block
data = buffer[pos..-1]
# replace it in the original with empty string
buffer[pos..-1] = ''
data
end
def erb_content_for(name, &block)
eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_erb(&block)"
end
def block_content_for(name, &block)
eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_block(&block)"
end end
end end
end end

View file

@ -3,18 +3,16 @@ require 'action_view/helpers/tag_helper'
module ActionView module ActionView
module Helpers module Helpers
# The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the select-type methods # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the
# share a number of common options that are as follows: # 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 # * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday"
# birthday[month] instead of date[month] if passed to the select_month method. # 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>: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 # * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true,
# method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead of "date[month]". # the select_month method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead of
# "date[month]".
module DateHelper module DateHelper
include ActionView::Helpers::TagHelper
DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX')
# Reports the approximate distance in time between two Time or Date objects or integers as seconds. # Reports the approximate distance in time between two Time or Date objects or integers as seconds.
# Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs # Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs
# Distances are reported based on the following table: # Distances are reported based on the following table:
@ -51,40 +49,45 @@ module ActionView
# distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute # distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute
# distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute
# distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year
# distance_of_time_in_words(from_time, from_time + 4.years + 15.days + 30.minutes + 5.seconds) # => over 4 years # distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => over 4 years
# #
# to_time = Time.now + 6.years + 19.days # to_time = Time.now + 6.years + 19.days
# distance_of_time_in_words(from_time, to_time, true) # => over 6 years # distance_of_time_in_words(from_time, to_time, true) # => over 6 years
# distance_of_time_in_words(to_time, from_time, true) # => over 6 years # distance_of_time_in_words(to_time, from_time, true) # => over 6 years
# distance_of_time_in_words(Time.now, Time.now) # => less than a minute # distance_of_time_in_words(Time.now, Time.now) # => less than a minute
# #
def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false) def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {})
from_time = from_time.to_time if from_time.respond_to?(:to_time) from_time = from_time.to_time if from_time.respond_to?(:to_time)
to_time = to_time.to_time if to_time.respond_to?(:to_time) to_time = to_time.to_time if to_time.respond_to?(:to_time)
distance_in_minutes = (((to_time - from_time).abs)/60).round distance_in_minutes = (((to_time - from_time).abs)/60).round
distance_in_seconds = ((to_time - from_time).abs).round distance_in_seconds = ((to_time - from_time).abs).round
I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
case distance_in_minutes case distance_in_minutes
when 0..1 when 0..1
return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds return distance_in_minutes == 0 ?
locale.t(:less_than_x_minutes, :count => 1) :
locale.t(:x_minutes, :count => distance_in_minutes) unless include_seconds
case distance_in_seconds case distance_in_seconds
when 0..4 then 'less than 5 seconds' when 0..4 then locale.t :less_than_x_seconds, :count => 5
when 5..9 then 'less than 10 seconds' when 5..9 then locale.t :less_than_x_seconds, :count => 10
when 10..19 then 'less than 20 seconds' when 10..19 then locale.t :less_than_x_seconds, :count => 20
when 20..39 then 'half a minute' when 20..39 then locale.t :half_a_minute
when 40..59 then 'less than a minute' when 40..59 then locale.t :less_than_x_minutes, :count => 1
else '1 minute' else locale.t :x_minutes, :count => 1
end end
when 2..44 then "#{distance_in_minutes} minutes" when 2..44 then locale.t :x_minutes, :count => distance_in_minutes
when 45..89 then 'about 1 hour' when 45..89 then locale.t :about_x_hours, :count => 1
when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours" when 90..1439 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round
when 1440..2879 then '1 day' when 1440..2879 then locale.t :x_days, :count => 1
when 2880..43199 then "#{(distance_in_minutes / 1440).round} days" when 2880..43199 then locale.t :x_days, :count => (distance_in_minutes / 1440).round
when 43200..86399 then 'about 1 month' when 43200..86399 then locale.t :about_x_months, :count => 1
when 86400..525599 then "#{(distance_in_minutes / 43200).round} months" when 86400..525599 then locale.t :x_months, :count => (distance_in_minutes / 43200).round
when 525600..1051199 then 'about 1 year' when 525600..1051199 then locale.t :about_x_years, :count => 1
else "over #{(distance_in_minutes / 525600).round} years" else locale.t :over_x_years, :count => (distance_in_minutes / 525600).round
end
end end
end end
@ -102,17 +105,37 @@ module ActionView
alias_method :distance_of_time_in_words_to_now, :time_ago_in_words alias_method :distance_of_time_in_words_to_now, :time_ago_in_words
# Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute (identified by # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based
# +method+) on an object assigned to the template (identified by +object+). It's possible to tailor the selects through the +options+ hash, # attribute (identified by +method+) on an object assigned to the template (identified by +object+). You can
# which accepts all the keys that each of the individual select builders do (like <tt>:use_month_numbers</tt> for select_month) as well as a range of # the output in the +options+ hash.
# discard options. The discard options are <tt>:discard_year</tt>, <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. It's also possible to explicitly
# set the order of the tags using the <tt>:order</tt> option with an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in
# the desired order. Symbols may be omitted and the respective select is not included.
# #
# Pass the <tt>:default</tt> option to set the default date. Use a Time object or a Hash of <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt>, and <tt>:second</tt>. # ==== Options
# # * <tt>:use_month_numbers</tt> - Set to true if you want to use month numbers rather than month names (e.g.
# Passing <tt>:disabled => true</tt> as part of the +options+ will make elements inaccessible for change. # "2" instead of "February").
# * <tt>:use_short_month</tt> - Set to true if you want to use the abbreviated month name instead of the full
# name (e.g. "Feb" instead of "February").
# * <tt>:add_month_number</tt> - Set to true if you want to show both, the month's number and name (e.g.
# "2 - February" instead of "February").
# * <tt>:use_month_names</tt> - Set to an array with 12 month names if you want to customize month names.
# Note: You can also use Rails' new i18n functionality for this.
# * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing).
# * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Time.now.year - 5</tt>.
# * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Time.now.year + 5</tt>.
# * <tt>:discard_day</tt> - Set to true if you don't want to show a day select. This includes the day
# as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the
# first of the given month in order to not create invalid dates like 31 February.
# * <tt>:discard_month</tt> - Set to true if you don't want to show a month select. This includes the month
# as a hidden field instead of showing a select field. Also note that this implicitly sets :discard_day to true.
# * <tt>:discard_year</tt> - Set to true if you don't want to show a year select. This includes the year
# as a hidden field instead of showing a select field.
# * <tt>:order</tt> - Set to an array containing <tt>:day</tt>, <tt>:month</tt> and <tt>:year</tt> do
# customize the order in which the select fields are shown. If you leave out any of the symbols, the respective
# select will not be shown (like when you set <tt>:discard_xxx => true</tt>. Defaults to the order defined in
# the respective locale (e.g. [:year, :month, :day] in the en-US locale that ships with Rails).
# * <tt>:include_blank</tt> - Include a blank option in every select field so it's possible to set empty
# dates.
# * <tt>:default</tt> - Set a default date if the affected date isn't set or is nil.
# * <tt>:disabled</tt> - Set to true if you want show the select fields as disabled.
# #
# If anything is passed in the +html_options+ hash it will be applied to every select tag in the set. # If anything is passed in the +html_options+ hash it will be applied to every select tag in the set.
# #
@ -150,15 +173,18 @@ module ActionView
# #
# The selects are prepared for multi-parameter assignment to an Active Record object. # The selects are prepared for multi-parameter assignment to an Active Record object.
# #
# Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that all month # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
# choices are valid. # all month choices are valid.
def date_select(object_name, method, options = {}, html_options = {}) def date_select(object_name, method, options = {}, html_options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_date_select_tag(options, html_options) InstanceTag.new(object_name, method, self, options.delete(:object)).to_date_select_tag(options, html_options)
end end
# Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a specified # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a
# time-based attribute (identified by +method+) on an object assigned to the template (identified by +object+). # specified time-based attribute (identified by +method+) on an object assigned to the template (identified by
# You can include the seconds with <tt>:include_seconds</tt>. # +object+). You can include the seconds with <tt>:include_seconds</tt>.
#
# This method will also generate 3 input hidden tags, for the actual year, month and day unless the option
# <tt>:ignore_date</tt> is set to +true+.
# #
# If anything is passed in the html_options hash it will be applied to every select tag in the set. # If anything is passed in the html_options hash it will be applied to every select tag in the set.
# #
@ -166,7 +192,8 @@ module ActionView
# # Creates a time select tag that, when POSTed, will be stored in the post variable in the sunrise attribute # # Creates a time select tag that, when POSTed, will be stored in the post variable in the sunrise attribute
# time_select("post", "sunrise") # time_select("post", "sunrise")
# #
# # Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted attribute # # Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted
# # attribute
# time_select("order", "submitted") # time_select("order", "submitted")
# #
# # Creates a time select tag that, when POSTed, will be stored in the mail variable in the sent_at attribute # # Creates a time select tag that, when POSTed, will be stored in the mail variable in the sent_at attribute
@ -185,43 +212,46 @@ module ActionView
# #
# The selects are prepared for multi-parameter assignment to an Active Record object. # The selects are prepared for multi-parameter assignment to an Active Record object.
# #
# Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that all month # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
# choices are valid. # all month choices are valid.
def time_select(object_name, method, options = {}, html_options = {}) def time_select(object_name, method, options = {}, html_options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_time_select_tag(options, html_options) InstanceTag.new(object_name, method, self, options.delete(:object)).to_time_select_tag(options, html_options)
end end
# Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a
# attribute (identified by +method+) on an object assigned to the template (identified by +object+). Examples: # specified datetime-based attribute (identified by +method+) on an object assigned to the template (identified
# by +object+). Examples:
# #
# If anything is passed in the html_options hash it will be applied to every select tag in the set. # If anything is passed in the html_options hash it will be applied to every select tag in the set.
# #
# ==== Examples # ==== Examples
# # Generates a datetime select that, when POSTed, will be stored in the post variable in the written_on attribute # # Generates a datetime select that, when POSTed, will be stored in the post variable in the written_on
# # attribute
# datetime_select("post", "written_on") # datetime_select("post", "written_on")
# #
# # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the # # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the
# # post variable in the written_on attribute. # # post variable in the written_on attribute.
# datetime_select("post", "written_on", :start_year => 1995) # datetime_select("post", "written_on", :start_year => 1995)
# #
# # Generates a datetime select with a default value of 3 days from the current time that, when POSTed, will be stored in the # # Generates a datetime select with a default value of 3 days from the current time that, when POSTed, will
# # trip variable in the departing attribute. # # be stored in the trip variable in the departing attribute.
# datetime_select("trip", "departing", :default => 3.days.from_now) # datetime_select("trip", "departing", :default => 3.days.from_now)
# #
# # Generates a datetime select that discards the type that, when POSTed, will be stored in the post variable as the written_on # # Generates a datetime select that discards the type that, when POSTed, will be stored in the post variable
# # attribute. # # as the written_on attribute.
# datetime_select("post", "written_on", :discard_type => true) # datetime_select("post", "written_on", :discard_type => true)
# #
# The selects are prepared for multi-parameter assignment to an Active Record object. # The selects are prepared for multi-parameter assignment to an Active Record object.
def datetime_select(object_name, method, options = {}, html_options = {}) def datetime_select(object_name, method, options = {}, html_options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_datetime_select_tag(options, html_options) InstanceTag.new(object_name, method, self, options.delete(:object)).to_datetime_select_tag(options, html_options)
end end
# Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the +datetime+. # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the
# It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of # +datetime+. It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with
# symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, it # an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not
# will be appended onto the <tt>:order</tt> passed in. You can also add <tt>:date_separator</tt> and <tt>:time_separator</tt> # supply a Symbol, it will be appended onto the <tt>:order</tt> passed in. You can also add
# keys to the +options+ to control visual display of the elements. # <tt>:date_separator</tt>, <tt>:datetime_separator</tt> and <tt>:time_separator</tt> keys to the +options+ to
# control visual display of the elements.
# #
# If anything is passed in the html_options hash it will be applied to every select tag in the set. # If anything is passed in the html_options hash it will be applied to every select tag in the set.
# #
@ -242,6 +272,11 @@ module ActionView
# # with a '/' between each date field. # # with a '/' between each date field.
# select_datetime(my_date_time, :date_separator => '/') # select_datetime(my_date_time, :date_separator => '/')
# #
# # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
# # with a date fields separated by '/', time fields separated by '' and the date and time fields
# # separated by a comma (',').
# select_datetime(my_date_time, :date_separator => '/', :time_separator => '', :datetime_separator => ',')
#
# # Generates a datetime select that discards the type of the field and defaults to the datetime in # # Generates a datetime select that discards the type of the field and defaults to the datetime in
# # my_date_time (four days after today) # # my_date_time (four days after today)
# select_datetime(my_date_time, :discard_type => true) # select_datetime(my_date_time, :discard_type => true)
@ -251,14 +286,13 @@ module ActionView
# select_datetime(my_date_time, :prefix => 'payday') # select_datetime(my_date_time, :prefix => 'payday')
# #
def select_datetime(datetime = Time.current, options = {}, html_options = {}) def select_datetime(datetime = Time.current, options = {}, html_options = {})
separator = options[:datetime_separator] || '' DateTimeSelector.new(datetime, options, html_options).select_datetime
select_date(datetime, options, html_options) + separator + select_time(datetime, options, html_options)
end end
# Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+. # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+.
# It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of # It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of
# symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, it # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol,
# will be appended onto the <tt>:order</tt> passed in. # it will be appended onto the <tt>:order</tt> passed in.
# #
# If anything is passed in the html_options hash it will be applied to every select tag in the set. # If anything is passed in the html_options hash it will be applied to every select tag in the set.
# #
@ -277,21 +311,18 @@ module ActionView
# #
# # Generates a date select that discards the type of the field and defaults to the date in # # Generates a date select that discards the type of the field and defaults to the date in
# # my_date (six days after today) # # my_date (six days after today)
# select_datetime(my_date_time, :discard_type => true) # select_date(my_date, :discard_type => true)
#
# # Generates a date select that defaults to the date in my_date,
# # which has fields separated by '/'
# select_date(my_date, :date_separator => '/')
# #
# # Generates a date select that defaults to the datetime in my_date (six days after today) # # Generates a date select that defaults to the datetime in my_date (six days after today)
# # prefixed with 'payday' rather than 'date' # # prefixed with 'payday' rather than 'date'
# select_datetime(my_date_time, :prefix => 'payday') # select_date(my_date, :prefix => 'payday')
# #
def select_date(date = Date.current, options = {}, html_options = {}) def select_date(date = Date.current, options = {}, html_options = {})
options[:order] ||= [] DateTimeSelector.new(date, options, html_options).select_date
[:year, :month, :day].each { |o| options[:order].push(o) unless options[:order].include?(o) }
select_date = ''
options[:order].each do |o|
select_date << self.send("select_#{o}", date, options, html_options)
end
select_date
end end
# Returns a set of html select-tags (one for hour and minute) # Returns a set of html select-tags (one for hour and minute)
@ -322,8 +353,7 @@ module ActionView
# select_time(my_time, :time_separator => ':', :include_seconds => true) # select_time(my_time, :time_separator => ':', :include_seconds => true)
# #
def select_time(datetime = Time.current, options = {}, html_options = {}) def select_time(datetime = Time.current, options = {}, html_options = {})
separator = options[:time_separator] || '' DateTimeSelector.new(datetime, options, html_options).select_time
select_hour(datetime, options, html_options) + separator + select_minute(datetime, options, html_options) + (options[:include_seconds] ? separator + select_second(datetime, options, html_options) : '')
end end
# Returns a select tag with options for each of the seconds 0 through 59 with the current second selected. # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected.
@ -344,25 +374,12 @@ module ActionView
# select_second(my_time, :field_name => 'interval') # select_second(my_time, :field_name => 'interval')
# #
def select_second(datetime, options = {}, html_options = {}) def select_second(datetime, options = {}, html_options = {})
val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) : '' DateTimeSelector.new(datetime, options, html_options).select_second
if options[:use_hidden]
options[:include_seconds] ? hidden_html(options[:field_name] || 'second', val, options) : ''
else
second_options = []
0.upto(59) do |second|
second_options << ((val == second) ?
content_tag(:option, leading_zero_on_single_digits(second), :value => leading_zero_on_single_digits(second), :selected => "selected") :
content_tag(:option, leading_zero_on_single_digits(second), :value => leading_zero_on_single_digits(second))
)
second_options << "\n"
end
select_html(options[:field_name] || 'second', second_options.join, options, html_options)
end
end end
# Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected. # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected.
# Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute selected # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute
# The <tt>minute</tt> can also be substituted for a minute number. # selected. The <tt>minute</tt> can also be substituted for a minute number.
# Override the field name using the <tt>:field_name</tt> option, 'minute' by default. # Override the field name using the <tt>:field_name</tt> option, 'minute' by default.
# #
# ==== Examples # ==== Examples
@ -379,20 +396,7 @@ module ActionView
# select_minute(my_time, :field_name => 'stride') # select_minute(my_time, :field_name => 'stride')
# #
def select_minute(datetime, options = {}, html_options = {}) def select_minute(datetime, options = {}, html_options = {})
val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.min) : '' DateTimeSelector.new(datetime, options, html_options).select_minute
if options[:use_hidden]
hidden_html(options[:field_name] || 'minute', val, options)
else
minute_options = []
0.step(59, options[:minute_step] || 1) do |minute|
minute_options << ((val == minute) ?
content_tag(:option, leading_zero_on_single_digits(minute), :value => leading_zero_on_single_digits(minute), :selected => "selected") :
content_tag(:option, leading_zero_on_single_digits(minute), :value => leading_zero_on_single_digits(minute))
)
minute_options << "\n"
end
select_html(options[:field_name] || 'minute', minute_options.join, options, html_options)
end
end end
# Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected.
@ -402,31 +406,18 @@ module ActionView
# ==== Examples # ==== Examples
# my_time = Time.now + 6.hours # my_time = Time.now + 6.hours
# #
# # Generates a select field for minutes that defaults to the minutes for the time in my_time # # Generates a select field for hours that defaults to the hour for the time in my_time
# select_minute(my_time) # select_hour(my_time)
# #
# # Generates a select field for minutes that defaults to the number given # # Generates a select field for hours that defaults to the number given
# select_minute(14) # select_hour(13)
# #
# # Generates a select field for minutes that defaults to the minutes for the time in my_time # # Generates a select field for hours that defaults to the minutes for the time in my_time
# # that is named 'stride' rather than 'second' # # that is named 'stride' rather than 'second'
# select_minute(my_time, :field_name => 'stride') # select_hour(my_time, :field_name => 'stride')
# #
def select_hour(datetime, options = {}, html_options = {}) def select_hour(datetime, options = {}, html_options = {})
val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) : '' DateTimeSelector.new(datetime, options, html_options).select_hour
if options[:use_hidden]
hidden_html(options[:field_name] || 'hour', val, options)
else
hour_options = []
0.upto(23) do |hour|
hour_options << ((val == hour) ?
content_tag(:option, leading_zero_on_single_digits(hour), :value => leading_zero_on_single_digits(hour), :selected => "selected") :
content_tag(:option, leading_zero_on_single_digits(hour), :value => leading_zero_on_single_digits(hour))
)
hour_options << "\n"
end
select_html(options[:field_name] || 'hour', hour_options.join, options, html_options)
end
end end
# Returns a select tag with options for each of the days 1 through 31 with the current day selected. # Returns a select tag with options for each of the days 1 through 31 with the current day selected.
@ -447,30 +438,17 @@ module ActionView
# select_day(my_time, :field_name => 'due') # select_day(my_time, :field_name => 'due')
# #
def select_day(date, options = {}, html_options = {}) def select_day(date, options = {}, html_options = {})
val = date ? (date.kind_of?(Fixnum) ? date : date.day) : '' DateTimeSelector.new(date, options, html_options).select_day
if options[:use_hidden]
hidden_html(options[:field_name] || 'day', val, options)
else
day_options = []
1.upto(31) do |day|
day_options << ((val == day) ?
content_tag(:option, day, :value => day, :selected => "selected") :
content_tag(:option, day, :value => day)
)
day_options << "\n"
end
select_html(options[:field_name] || 'day', day_options.join, options, html_options)
end
end end
# Returns a select tag with options for each of the months January through December with the current month selected. # Returns a select tag with options for each of the months January through December with the current month
# The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are used as values # selected. The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are
# (what's submitted to the server). It's also possible to use month numbers for the presentation instead of names -- # used as values (what's submitted to the server). It's also possible to use month numbers for the presentation
# set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you want both numbers and names, # instead of names -- set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you
# set the <tt>:add_month_numbers</tt> key in +options+ to true. If you would prefer to show month names as abbreviations, # want both numbers and names, set the <tt>:add_month_numbers</tt> key in +options+ to true. If you would prefer
# set the <tt>:use_short_month</tt> key in +options+ to true. If you want to use your own month names, set the # to show month names as abbreviations, set the <tt>:use_short_month</tt> key in +options+ to true. If you want
# <tt>:use_month_names</tt> key in +options+ to an array of 12 month names. Override the field name using the # to use your own month names, set the <tt>:use_month_names</tt> key in +options+ to an array of 12 month names.
# <tt>:field_name</tt> option, 'month' by default. # Override the field name using the <tt>:field_name</tt> option, 'month' by default.
# #
# ==== Examples # ==== Examples
# # Generates a select field for months that defaults to the current month that # # Generates a select field for months that defaults to the current month that
@ -498,36 +476,14 @@ module ActionView
# select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...)) # select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...))
# #
def select_month(date, options = {}, html_options = {}) def select_month(date, options = {}, html_options = {})
val = date ? (date.kind_of?(Fixnum) ? date : date.month) : '' DateTimeSelector.new(date, options, html_options).select_month
if options[:use_hidden]
hidden_html(options[:field_name] || 'month', val, options)
else
month_options = []
month_names = options[:use_month_names] || (options[:use_short_month] ? Date::ABBR_MONTHNAMES : Date::MONTHNAMES)
month_names.unshift(nil) if month_names.size < 13
1.upto(12) do |month_number|
month_name = if options[:use_month_numbers]
month_number
elsif options[:add_month_numbers]
month_number.to_s + ' - ' + month_names[month_number]
else
month_names[month_number]
end end
month_options << ((val == month_number) ? # Returns a select tag with options for each of the five years on each side of the current, which is selected.
content_tag(:option, month_name, :value => month_number, :selected => "selected") : # The five year radius can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the
content_tag(:option, month_name, :value => month_number) # +options+. Both ascending and descending year lists are supported by making <tt>:start_year</tt> less than or
) # greater than <tt>:end_year</tt>. The <tt>date</tt> can also be substituted for a year given as a number.
month_options << "\n" # Override the field name using the <tt>:field_name</tt> option, 'year' by default.
end
select_html(options[:field_name] || 'month', month_options.join, options, html_options)
end
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+. Both ascending and descending year
# lists are supported by making <tt>:start_year</tt> less than or greater than <tt>:end_year</tt>. The <tt>date</tt> can also be
# substituted for a year given as a number. Override the field name using the <tt>:field_name</tt> option, 'year' by default.
# #
# ==== Examples # ==== Examples
# # Generates a select field for years that defaults to the current year that # # Generates a select field for years that defaults to the current year that
@ -547,138 +503,369 @@ module ActionView
# select_year(2006, :start_year => 2000, :end_year => 2010) # select_year(2006, :start_year => 2000, :end_year => 2010)
# #
def select_year(date, options = {}, html_options = {}) def select_year(date, options = {}, html_options = {})
val = date ? (date.kind_of?(Fixnum) ? date : date.year) : '' DateTimeSelector.new(date, options, html_options).select_year
if options[:use_hidden]
hidden_html(options[:field_name] || 'year', val, options)
else
year_options = []
y = date ? (date.kind_of?(Fixnum) ? (y = (date == 0) ? Date.today.year : date) : date.year) : Date.today.year
start_year, end_year = (options[:start_year] || y-5), (options[:end_year] || y+5)
step_val = start_year < end_year ? 1 : -1
start_year.step(end_year, step_val) do |year|
year_options << ((val == year) ?
content_tag(:option, year, :value => year, :selected => "selected") :
content_tag(:option, year, :value => year)
)
year_options << "\n"
end end
select_html(options[:field_name] || 'year', year_options.join, options, html_options) end
class DateTimeSelector #:nodoc:
extend ActiveSupport::Memoizable
include ActionView::Helpers::TagHelper
DEFAULT_PREFIX = 'date'.freeze unless const_defined?('DEFAULT_PREFIX')
POSITION = {
:year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6
}.freeze unless const_defined?('POSITION')
def initialize(datetime, options = {}, html_options = {})
@options = options.dup
@html_options = html_options.dup
@datetime = datetime
end
def select_datetime
# TODO: Remove tag conditional
# Ideally we could just join select_date and select_date for the tag case
if @options[:tag] && @options[:ignore_date]
select_time
elsif @options[:tag]
order = date_order.dup
order -= [:hour, :minute, :second]
@options[:discard_year] ||= true unless order.include?(:year)
@options[:discard_month] ||= true unless order.include?(:month)
@options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
@options[:discard_minute] ||= true if @options[:discard_hour]
@options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute]
# If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are
# valid (otherwise it could be 31 and february wouldn't be a valid date)
if @options[:discard_day] && !@options[:discard_month]
@datetime = @datetime.change(:day => 1)
end
[:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
order += [:hour, :minute, :second] unless @options[:discard_hour]
build_selects_from_types(order)
else
"#{select_date}#{@options[:datetime_separator]}#{select_time}"
end
end
def select_date
order = date_order.dup
# TODO: Remove tag conditional
if @options[:tag]
@options[:discard_hour] = true
@options[:discard_minute] = true
@options[:discard_second] = true
@options[:discard_year] ||= true unless order.include?(:year)
@options[:discard_month] ||= true unless order.include?(:month)
@options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
# If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are
# valid (otherwise it could be 31 and february wouldn't be a valid date)
if @options[:discard_day] && !@options[:discard_month]
@datetime = @datetime.change(:day => 1)
end
end
[:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
build_selects_from_types(order)
end
def select_time
order = []
# TODO: Remove tag conditional
if @options[:tag]
@options[:discard_month] = true
@options[:discard_year] = true
@options[:discard_day] = true
@options[:discard_second] ||= true unless @options[:include_seconds]
order += [:year, :month, :day] unless @options[:ignore_date]
end
order += [:hour, :minute]
order << :second if @options[:include_seconds]
build_selects_from_types(order)
end
def select_second
if @options[:use_hidden] || @options[:discard_second]
build_hidden(:second, sec) if @options[:include_seconds]
else
build_options_and_select(:second, sec)
end
end
def select_minute
if @options[:use_hidden] || @options[:discard_minute]
build_hidden(:minute, min)
else
build_options_and_select(:minute, min, :step => @options[:minute_step])
end
end
def select_hour
if @options[:use_hidden] || @options[:discard_hour]
build_hidden(:hour, hour)
else
build_options_and_select(:hour, hour, :end => 23)
end
end
def select_day
if @options[:use_hidden] || @options[:discard_day]
build_hidden(:day, day)
else
build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false)
end
end
def select_month
if @options[:use_hidden] || @options[:discard_month]
build_hidden(:month, month)
else
month_options = []
1.upto(12) do |month_number|
options = { :value => month_number }
options[:selected] = "selected" if month == month_number
month_options << content_tag(:option, month_name(month_number), options) + "\n"
end
build_select(:month, month_options.join)
end
end
def select_year
if !@datetime || @datetime == 0
val = ''
middle_year = Date.today.year
else
val = middle_year = year
end
if @options[:use_hidden] || @options[:discard_year]
build_hidden(:year, val)
else
options = {}
options[:start] = @options[:start_year] || middle_year - 5
options[:end] = @options[:end_year] || middle_year + 5
options[:step] = options[:start] < options[:end] ? 1 : -1
options[:leading_zeros] = false
build_options_and_select(:year, val, options)
end end
end end
private private
%w( sec min hour day month year ).each do |method|
define_method(method) do
@datetime.kind_of?(Fixnum) ? @datetime : @datetime.send(method) if @datetime
end
end
# Returns translated month names, but also ensures that a custom month
# name array has a leading nil element
def month_names
month_names = @options[:use_month_names] || translated_month_names
month_names.unshift(nil) if month_names.size < 13
month_names
end
memoize :month_names
# Returns translated month names
# => [nil, "January", "February", "March",
# "April", "May", "June", "July",
# "August", "September", "October",
# "November", "December"]
#
# If :use_short_month option is set
# => [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun",
# "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
def translated_month_names
begin
key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names'
I18n.translate(key, :locale => @options[:locale])
end
end
# Lookup month name for number
# month_name(1) => "January"
#
# If :use_month_numbers option is passed
# month_name(1) => 1
#
# If :add_month_numbers option is passed
# month_name(1) => "1 - January"
def month_name(number)
if @options[:use_month_numbers]
number
elsif @options[:add_month_numbers]
"#{number} - #{month_names[number]}"
else
month_names[number]
end
end
def date_order
@options[:order] || translated_date_order
end
memoize :date_order
def translated_date_order
begin
I18n.translate(:'date.order', :locale => @options[:locale]) || []
end
end
# Build full select tag from date type and options
def build_options_and_select(type, selected, options = {})
build_select(type, build_options(selected, options))
end
# Build select option html from date value and options
# build_options(15, :start => 1, :end => 31)
# => "<option value="1">1</option>
# <option value=\"2\">2</option>
# <option value=\"3\">3</option>..."
def build_options(selected, options = {})
start = options.delete(:start) || 0
stop = options.delete(:end) || 59
step = options.delete(:step) || 1
leading_zeros = options.delete(:leading_zeros).nil? ? true : false
select_options = []
start.step(stop, step) do |i|
value = leading_zeros ? sprintf("%02d", i) : i
tag_options = { :value => value }
tag_options[:selected] = "selected" if selected == i
select_options << content_tag(:option, value, tag_options)
end
select_options.join("\n") + "\n"
end
# Builds select tag from date type and html select options
# build_select(:month, "<option value="1">January</option>...")
# => "<select id="post_written_on_2i" name="post[written_on(2i)]">
# <option value="1">January</option>...
# </select>"
def build_select(type, select_options_as_html)
select_options = {
:id => input_id_from_type(type),
:name => input_name_from_type(type)
}.merge(@html_options)
select_options.merge!(:disabled => 'disabled') if @options[:disabled]
def select_html(type, html_options, options, select_tag_options = {})
name_and_id_from_options(options, type)
select_options = {:id => options[:id], :name => options[:name]}
select_options.merge!(:disabled => 'disabled') if options[:disabled]
select_options.merge!(select_tag_options) unless select_tag_options.empty?
select_html = "\n" select_html = "\n"
select_html << content_tag(:option, '', :value => '') + "\n" if options[:include_blank] select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank]
select_html << html_options.to_s select_html << select_options_as_html.to_s
content_tag(:select, select_html, select_options) + "\n" content_tag(:select, select_html, select_options) + "\n"
end end
def hidden_html(type, value, options) # Builds hidden input tag for date part and value
name_and_id_from_options(options, type) # build_hidden(:year, 2008)
hidden_html = tag(:input, :type => "hidden", :id => options[:id], :name => options[:name], :value => value) + "\n" # => "<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="2008" />"
def build_hidden(type, value)
tag(:input, {
:type => "hidden",
:id => input_id_from_type(type),
:name => input_name_from_type(type),
:value => value
}) + "\n"
end end
def name_and_id_from_options(options, type) # Returns the name attribute for the input tag
options[:name] = (options[:prefix] || DEFAULT_PREFIX) + (options[:discard_type] ? '' : "[#{type}]") # => post[written_on(1i)]
options[:id] = options[:name].gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '') def input_name_from_type(type)
prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX
prefix += "[#{@options[:index]}]" if @options[:index]
field_name = @options[:field_name] || type
if @options[:include_position]
field_name += "(#{ActionView::Helpers::DateTimeSelector::POSITION[type]}i)"
end end
def leading_zero_on_single_digits(number) @options[:discard_type] ? prefix : "#{prefix}[#{field_name}]"
number > 9 ? number : "0#{number}" end
# Returns the id attribute for the input tag
# => "post_written_on_1i"
def input_id_from_type(type)
input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '')
end
# Given an ordering of datetime components, create the selection html
# and join them with their appropriate seperators
def build_selects_from_types(order)
select = ''
order.reverse.each do |type|
separator = separator(type) unless type == order.first # don't add on last field
select.insert(0, separator.to_s + send("select_#{type}").to_s)
end
select
end
# Returns the separator for a given datetime component
def separator(type)
case type
when :month, :day
@options[:date_separator]
when :hour
(@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator]
when :minute
@options[:time_separator]
when :second
@options[:include_seconds] ? @options[:time_separator] : ""
end
end end
end end
class InstanceTag #:nodoc: class InstanceTag #:nodoc:
include DateHelper
def to_date_select_tag(options = {}, html_options = {}) def to_date_select_tag(options = {}, html_options = {})
date_or_time_select(options.merge(:discard_hour => true), html_options) datetime_selector(options, html_options).select_date
end end
def to_time_select_tag(options = {}, html_options = {}) def to_time_select_tag(options = {}, html_options = {})
date_or_time_select(options.merge(:discard_year => true, :discard_month => true), html_options) datetime_selector(options, html_options).select_time
end end
def to_datetime_select_tag(options = {}, html_options = {}) def to_datetime_select_tag(options = {}, html_options = {})
date_or_time_select(options, html_options) datetime_selector(options, html_options).select_datetime
end end
private private
def date_or_time_select(options, html_options = {}) def datetime_selector(options, html_options)
defaults = { :discard_type => true } datetime = value(object) || default_datetime(options)
options = defaults.merge(options)
datetime = value(object)
datetime ||= default_time_from_options(options[:default]) unless options[:include_blank]
position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 } options = options.dup
options[:field_name] = @method_name
options[:include_position] = true
options[:prefix] ||= @object_name
options[:index] ||= @auto_index
options[:datetime_separator] ||= ' &mdash; '
options[:time_separator] ||= ' : '
order = (options[:order] ||= [:year, :month, :day]) DateTimeSelector.new(datetime, options.merge(:tag => true), html_options)
# Discard explicit and implicit by not being included in the :order
discard = {}
discard[:year] = true if options[:discard_year] or !order.include?(:year)
discard[:month] = true if options[:discard_month] or !order.include?(:month)
discard[:day] = true if options[:discard_day] or discard[:month] or !order.include?(:day)
discard[:hour] = true if options[:discard_hour]
discard[:minute] = true if options[:discard_minute] or discard[:hour]
discard[:second] = true unless options[:include_seconds] && !discard[:minute]
# If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are valid
# (otherwise it could be 31 and february wouldn't be a valid date)
if datetime && discard[:day] && !discard[:month]
datetime = datetime.change(:day => 1)
end end
# Maintain valid dates by including hidden fields for discarded elements def default_datetime(options)
[:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } return if options[:include_blank]
# Ensure proper ordering of :hour, :minute and :second case options[:default]
[:hour, :minute, :second].each { |o| order.delete(o); order.push(o) }
date_or_time_select = ''
order.reverse.each do |param|
# Send hidden fields for discarded elements once output has started
# This ensures AR can reconstruct valid dates using ParseDate
next if discard[param] && date_or_time_select.empty?
date_or_time_select.insert(0, self.send("select_#{param}", datetime, options_with_prefix(position[param], options.merge(:use_hidden => discard[param])), html_options))
date_or_time_select.insert(0,
case param
when :hour then (discard[:year] && discard[:day] ? "" : " &mdash; ")
when :minute then " : "
when :second then options[:include_seconds] ? " : " : ""
else ""
end)
end
date_or_time_select
end
def options_with_prefix(position, options)
prefix = "#{@object_name}"
if options[:index]
prefix << "[#{options[:index]}]"
elsif @auto_index
prefix << "[#{@auto_index}]"
end
options.merge(:prefix => "#{prefix}[#{@method_name}(#{position}i)]")
end
def default_time_from_options(default)
case default
when nil when nil
Time.current Time.current
when Date, Time when Date, Time
default options[:default]
else else
default = options[:default].dup
# Rename :minute and :second to :min and :sec # Rename :minute and :second to :min and :sec
default[:min] ||= default[:minute] default[:min] ||= default[:minute]
default[:sec] ||= default[:second] default[:sec] ||= default[:second]
@ -689,7 +876,10 @@ module ActionView
default[key] ||= time.send(key) default[key] ||= time.send(key)
end end
Time.utc_time(default[:year], default[:month], default[:day], default[:hour], default[:min], default[:sec]) Time.utc_time(
default[:year], default[:month], default[:day],
default[:hour], default[:min], default[:sec]
)
end end
end end
end end

View file

@ -2,21 +2,28 @@ module ActionView
module Helpers module Helpers
# Provides a set of methods for making it easier to debug Rails objects. # Provides a set of methods for making it easier to debug Rails objects.
module DebugHelper module DebugHelper
# Returns a <pre>-tag that has +object+ dumped by YAML. This creates a very # Returns a YAML representation of +object+ wrapped with <pre> and </pre>.
# readable way to inspect an object. # If the object cannot be converted to YAML using +to_yaml+, +inspect+ will be called instead.
# Useful for inspecting an object at the time of rendering.
# #
# ==== Example # ==== Example
# my_hash = {'first' => 1, 'second' => 'two', 'third' => [1,2,3]}
# debug(my_hash)
# #
# => <pre class='debug_dump'>--- # @user = User.new({ :username => 'testing', :password => 'xyz', :age => 42}) %>
# first: 1 # debug(@user)
# second: two # # =>
# third: # <pre class='debug_dump'>--- !ruby/object:User
# - 1 # attributes:
# - 2 # &nbsp; updated_at:
# - 3 # &nbsp; username: testing
#
# &nbsp; age: 42
# &nbsp; password: xyz
# &nbsp; created_at:
# attributes_cache: {}
#
# new_record: true
# </pre> # </pre>
def debug(object) def debug(object)
begin begin
Marshal::dump(object) Marshal::dump(object)

View file

@ -76,7 +76,7 @@ module ActionView
# Creates a form and a scope around a specific model object that is used as # Creates a form and a scope around a specific model object that is used as
# a base for questioning about values for the fields. # a base for questioning about values for the fields.
# #
# Rails provides succint resource-oriented form generation with +form_for+ # Rails provides succinct resource-oriented form generation with +form_for+
# like this: # like this:
# #
# <% form_for @offer do |f| %> # <% form_for @offer do |f| %>
@ -249,9 +249,9 @@ module ActionView
args.unshift object args.unshift object
end end
concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}), proc.binding) concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}))
fields_for(object_name, *(args << options), &proc) fields_for(object_name, *(args << options), &proc)
concat('</form>', proc.binding) concat('</form>')
end end
def apply_form_for_options!(object_or_array, options) #:nodoc: def apply_form_for_options!(object_or_array, options) #:nodoc:
@ -304,10 +304,6 @@ module ActionView
when String, Symbol when String, Symbol
object_name = record_or_name_or_array object_name = record_or_name_or_array
object = args.first object = args.first
when Array
object = record_or_name_or_array.last
object_name = ActionController::RecordIdentifier.singular_class_name(object)
apply_form_for_options!(record_or_name_or_array, options)
else else
object = record_or_name_or_array object = record_or_name_or_array
object_name = ActionController::RecordIdentifier.singular_class_name(object) object_name = ActionController::RecordIdentifier.singular_class_name(object)
@ -333,7 +329,7 @@ module ActionView
# # => <label for="post_title" class="title_label">A short title</label> # # => <label for="post_title" class="title_label">A short title</label>
# #
def label(object_name, method, text = nil, options = {}) def label(object_name, method, text = nil, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_label_tag(text, options) InstanceTag.new(object_name, method, self, options.delete(:object)).to_label_tag(text, options)
end end
# Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object
@ -355,7 +351,7 @@ module ActionView
# # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" /> # # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" />
# #
def text_field(object_name, method, options = {}) def text_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("text", options) InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("text", options)
end end
# Returns an input tag of the "password" type tailored for accessing a specified attribute (identified by +method+) on an object # Returns an input tag of the "password" type tailored for accessing a specified attribute (identified by +method+) on an object
@ -377,7 +373,7 @@ module ActionView
# # => <input type="text" id="account_pin" name="account[pin]" size="20" value="#{@account.pin}" class="form_input" /> # # => <input type="text" id="account_pin" name="account[pin]" size="20" value="#{@account.pin}" class="form_input" />
# #
def password_field(object_name, method, options = {}) def password_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("password", options) InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("password", options)
end end
# Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object # Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object
@ -395,7 +391,7 @@ module ActionView
# hidden_field(:user, :token) # hidden_field(:user, :token)
# # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" /> # # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
def hidden_field(object_name, method, options = {}) def hidden_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("hidden", options) InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("hidden", options)
end end
# Returns an file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object # Returns an file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object
@ -414,7 +410,7 @@ module ActionView
# # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" /> # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
# #
def file_field(object_name, method, options = {}) def file_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("file", options) InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("file", options)
end end
# Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+) # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
@ -442,15 +438,44 @@ module ActionView
# # #{@entry.body} # # #{@entry.body}
# # </textarea> # # </textarea>
def text_area(object_name, method, options = {}) def text_area(object_name, method, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_text_area_tag(options) InstanceTag.new(object_name, method, self, options.delete(:object)).to_text_area_tag(options)
end end
# Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object # 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 # assigned to the template (identified by +object+). This object must be an instance object (@object) and not a local object.
# integer is above zero, then the checkbox is checked. Additional options on the input tag can be passed as a # It's intended that +method+ returns an integer and if that integer is above zero, then the checkbox is checked.
# hash with +options+. The +checked_value+ defaults to 1 while the default +unchecked_value+ # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
# is set to 0 which is convenient for boolean values. Since HTTP standards say that unchecked checkboxes don't post anything, # while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
# we add a hidden value with the same name as the checkbox as a work around. #
# ==== Gotcha
#
# The HTML specification says unchecked check boxes are not successful, and
# thus web browsers do not send them. Unfortunately this introduces a gotcha:
# if an Invoice model has a +paid+ flag, and in the form that edits a paid
# invoice the user unchecks its check box, no +paid+ parameter is sent. So,
# any mass-assignment idiom like
#
# @invoice.update_attributes(params[:invoice])
#
# wouldn't update the flag.
#
# To prevent this the helper generates a hidden field with the same name as
# the checkbox after the very check box. So, the client either sends only the
# hidden field (representing the check box is unchecked), or both fields.
# Since the HTML specification says key/value pairs have to be sent in the
# same order they appear in the form and Rails parameters extraction always
# gets the first occurrence of any given key, that works in ordinary forms.
#
# Unfortunately that workaround does not work when the check box goes
# within an array-like parameter, as in
#
# <% fields_for "project[invoice_attributes][]", invoice, :index => nil do |form| %>
# <%= form.check_box :paid %>
# ...
# <% end %>
#
# because parameter name repetition is precisely what Rails seeks to distinguish
# the elements of the array.
# #
# ==== Examples # ==== Examples
# # Let's say that @post.validated? is 1: # # Let's say that @post.validated? is 1:
@ -468,7 +493,7 @@ module ActionView
# # <input name="eula[accepted]" type="hidden" value="no" /> # # <input name="eula[accepted]" type="hidden" value="no" />
# #
def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0") def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value) InstanceTag.new(object_name, method, self, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value)
end end
# Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object
@ -488,7 +513,7 @@ module ActionView
# # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" /> # # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" />
# # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" /> # # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" />
def radio_button(object_name, method, tag_value, options = {}) def radio_button(object_name, method, tag_value, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_radio_button_tag(tag_value, options) InstanceTag.new(object_name, method, self, options.delete(:object)).to_radio_button_tag(tag_value, options)
end end
end end
@ -501,12 +526,12 @@ module ActionView
DEFAULT_RADIO_OPTIONS = { }.freeze unless const_defined?(:DEFAULT_RADIO_OPTIONS) DEFAULT_RADIO_OPTIONS = { }.freeze unless const_defined?(:DEFAULT_RADIO_OPTIONS)
DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }.freeze unless const_defined?(:DEFAULT_TEXT_AREA_OPTIONS) DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }.freeze unless const_defined?(:DEFAULT_TEXT_AREA_OPTIONS)
def initialize(object_name, method_name, template_object, local_binding = nil, object = nil) def initialize(object_name, method_name, template_object, object = nil)
@object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
@template_object, @local_binding = template_object, local_binding @template_object = template_object
@object = object @object = object
if @object_name.sub!(/\[\]$/,"") if @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
if object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param) if (object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param)
@auto_index = object.to_param @auto_index = object.to_param
else else
raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
@ -683,7 +708,7 @@ module ActionView
end end
def sanitized_object_name def sanitized_object_name
@sanitized_object_name ||= @object_name.gsub(/[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "") @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
end end
def sanitized_method_name def sanitized_method_name
@ -701,6 +726,13 @@ module ActionView
def initialize(object_name, object, template, options, proc) def initialize(object_name, object, template, options, proc)
@object_name, @object, @template, @options, @proc = object_name, object, template, options, proc @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
@default_options = @options ? @options.slice(:index) : {} @default_options = @options ? @options.slice(:index) : {}
if @object_name.to_s.match(/\[\]$/)
if object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param)
@auto_index = object.to_param
else
raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
end
end
end end
(field_helpers - %w(label check_box radio_button fields_for)).each do |selector| (field_helpers - %w(label check_box radio_button fields_for)).each do |selector|
@ -713,16 +745,25 @@ module ActionView
end end
def fields_for(record_or_name_or_array, *args, &block) def fields_for(record_or_name_or_array, *args, &block)
if options.has_key?(:index)
index = "[#{options[:index]}]"
elsif defined?(@auto_index)
self.object_name = @object_name.to_s.sub(/\[\]$/,"")
index = "[#{@auto_index}]"
else
index = ""
end
case record_or_name_or_array case record_or_name_or_array
when String, Symbol when String, Symbol
name = "#{object_name}[#{record_or_name_or_array}]" name = "#{object_name}#{index}[#{record_or_name_or_array}]"
when Array when Array
object = record_or_name_or_array.last object = record_or_name_or_array.last
name = "#{object_name}[#{ActionController::RecordIdentifier.singular_class_name(object)}]" name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
args.unshift(object) args.unshift(object)
else else
object = record_or_name_or_array object = record_or_name_or_array
name = "#{object_name}[#{ActionController::RecordIdentifier.singular_class_name(object)}]" name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
args.unshift(object) args.unshift(object)
end end
@ -741,8 +782,8 @@ module ActionView
@template.radio_button(@object_name, method, tag_value, objectify_options(options)) @template.radio_button(@object_name, method, tag_value, objectify_options(options))
end end
def error_message_on(method, prepend_text = "", append_text = "", css_class = "formError") def error_message_on(method, *args)
@template.error_message_on(@object, method, prepend_text, append_text, css_class) @template.error_message_on(@object, method, *args)
end end
def error_messages(options = {}) def error_messages(options = {})

View file

@ -96,7 +96,7 @@ module ActionView
# By default, <tt>post.person_id</tt> is the selected option. Specify <tt>:selected => value</tt> to use a different selection # By default, <tt>post.person_id</tt> is the selected option. Specify <tt>:selected => value</tt> to use a different selection
# or <tt>:selected => nil</tt> to leave all options unselected. # or <tt>:selected => nil</tt> to leave all options unselected.
def select(object, method, choices, options = {}, html_options = {}) def select(object, method, choices, options = {}, html_options = {})
InstanceTag.new(object, method, self, nil, options.delete(:object)).to_select_tag(choices, options, html_options) InstanceTag.new(object, method, self, options.delete(:object)).to_select_tag(choices, options, html_options)
end end
# Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of # Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of
@ -130,12 +130,7 @@ module ActionView
# <option value="3">M. Clark</option> # <option value="3">M. Clark</option>
# </select> # </select>
def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {}) def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})
InstanceTag.new(object, method, self, nil, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options) InstanceTag.new(object, method, self, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options)
end
# Return select and option tags for the given object and method, using country_options_for_select to generate the list of option tags.
def country_select(object, method, priority_countries = nil, options = {}, html_options = {})
InstanceTag.new(object, method, self, nil, options.delete(:object)).to_country_select_tag(priority_countries, options, html_options)
end end
# Return select and option tags for the given object and method, using # Return select and option tags for the given object and method, using
@ -150,7 +145,8 @@ module ActionView
# You can also supply an array of TimeZone objects # You can also supply an array of TimeZone objects
# as +priority_zones+, so that they will be listed above the rest of the # as +priority_zones+, so that they will be listed above the rest of the
# (long) list. (You can use TimeZone.us_zones as a convenience for # (long) list. (You can use TimeZone.us_zones as a convenience for
# obtaining a list of the US time zones.) # obtaining a list of the US time zones, or a Regexp to select the zones
# of your choice)
# #
# Finally, this method supports a <tt>:default</tt> option, which selects # Finally, this method supports a <tt>:default</tt> option, which selects
# a default TimeZone if the object's time zone is +nil+. # a default TimeZone if the object's time zone is +nil+.
@ -164,9 +160,11 @@ module ActionView
# #
# time_zone_select( "user", 'time_zone', [ TimeZone['Alaska'], TimeZone['Hawaii'] ]) # time_zone_select( "user", 'time_zone', [ TimeZone['Alaska'], TimeZone['Hawaii'] ])
# #
# time_zone_select( "user", 'time_zone', /Australia/)
#
# time_zone_select( "user", "time_zone", TZInfo::Timezone.all.sort, :model => TZInfo::Timezone) # time_zone_select( "user", "time_zone", TZInfo::Timezone.all.sort, :model => TZInfo::Timezone)
def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {}) def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {})
InstanceTag.new(object, method, self, nil, options.delete(:object)).to_time_zone_select_tag(priority_zones, options, html_options) InstanceTag.new(object, method, self, options.delete(:object)).to_time_zone_select_tag(priority_zones, options, html_options)
end end
# Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container
@ -271,28 +269,13 @@ module ActionView
end end
end end
# Returns a string of option tags for pretty much any country in the world. Supply a country name as +selected+ to
# have it marked as the selected option tag. You can also supply an array of countries as +priority_countries+, so
# that they will be listed above the rest of the (long) list.
#
# NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
def country_options_for_select(selected = nil, priority_countries = nil)
country_options = ""
if priority_countries
country_options += options_for_select(priority_countries, selected)
country_options += "<option value=\"\" disabled=\"disabled\">-------------</option>\n"
end
return country_options + options_for_select(COUNTRIES, selected)
end
# Returns a string of option tags for pretty much any time zone in the # Returns a string of option tags for pretty much any time zone in the
# world. Supply a TimeZone name as +selected+ to have it marked as the # world. Supply a TimeZone name as +selected+ to have it marked as the
# selected option tag. You can also supply an array of TimeZone objects # selected option tag. You can also supply an array of TimeZone objects
# as +priority_zones+, so that they will be listed above the rest of the # as +priority_zones+, so that they will be listed above the rest of the
# (long) list. (You can use TimeZone.us_zones as a convenience for # (long) list. (You can use TimeZone.us_zones as a convenience for
# obtaining a list of the US time zones.) # obtaining a list of the US time zones, or a Regexp to select the zones
# of your choice)
# #
# The +selected+ parameter must be either +nil+, or a string that names # The +selected+ parameter must be either +nil+, or a string that names
# a TimeZone. # a TimeZone.
@ -311,6 +294,9 @@ module ActionView
convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } } convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } }
if priority_zones if priority_zones
if priority_zones.is_a?(Regexp)
priority_zones = model.all.find_all {|z| z =~ priority_zones}
end
zone_options += options_for_select(convert_zones[priority_zones], selected) zone_options += options_for_select(convert_zones[priority_zones], selected)
zone_options += "<option value=\"\" disabled=\"disabled\">-------------</option>\n" zone_options += "<option value=\"\" disabled=\"disabled\">-------------</option>\n"
@ -338,45 +324,6 @@ module ActionView
value == selected value == selected
end end
end end
# All the countries included in the country_options output.
COUNTRIES = ["Afghanistan", "Aland Islands", "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", "Burundi", "Cambodia",
"Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China",
"Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo",
"Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba",
"Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt",
"El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)",
"Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia",
"French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea",
"Guinea-Bissau", "Guyana", "Haiti", "Heard and McDonald Islands", "Holy See (Vatican City State)",
"Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq",
"Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya",
"Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan",
"Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya",
"Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic Of",
"Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique",
"Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of",
"Monaco", "Mongolia", "Montenegro", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru",
"Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger",
"Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau",
"Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines",
"Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation",
"Rwanda", "Saint Barthelemy", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia",
"Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino",
"Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore",
"Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa",
"South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname",
"Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic",
"Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste",
"Togo", "Tokelau", "Tonga", "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", "Venezuela",
"Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara",
"Yemen", "Zambia", "Zimbabwe"] unless const_defined?("COUNTRIES")
end end
class InstanceTag #:nodoc: class InstanceTag #:nodoc:
@ -399,18 +346,6 @@ module ActionView
) )
end end
def to_country_select_tag(priority_countries, options, html_options)
html_options = html_options.stringify_keys
add_default_name_and_id(html_options)
value = value(object)
content_tag("select",
add_options(
country_options_for_select(value, priority_countries),
options, value
), html_options
)
end
def to_time_zone_select_tag(priority_zones, options, html_options) def to_time_zone_select_tag(priority_zones, options, html_options)
html_options = html_options.stringify_keys html_options = html_options.stringify_keys
add_default_name_and_id(html_options) add_default_name_and_id(html_options)
@ -438,19 +373,15 @@ module ActionView
class FormBuilder class FormBuilder
def select(method, choices, options = {}, html_options = {}) def select(method, choices, options = {}, html_options = {})
@template.select(@object_name, method, choices, options.merge(:object => @object), html_options) @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options))
end end
def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
@template.collection_select(@object_name, method, collection, value_method, text_method, options.merge(:object => @object), html_options) @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
end
def country_select(method, priority_countries = nil, options = {}, html_options = {})
@template.country_select(@object_name, method, priority_countries, options.merge(:object => @object), html_options)
end end
def time_zone_select(method, priority_zones = nil, options = {}, html_options = {}) def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
@template.time_zone_select(@object_name, method, priority_zones, options.merge(:object => @object), html_options) @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options))
end end
end end
end end

View file

@ -62,7 +62,7 @@ module ActionView
# # <option>3</option><option>4</option></select> # # <option>3</option><option>4</option></select>
# #
# select_tag "colors", "<option>Red</option><option>Green</option><option>Blue</option>", :multiple => true # select_tag "colors", "<option>Red</option><option>Green</option><option>Blue</option>", :multiple => true
# # => <select id="colors" multiple="multiple" name="colors"><option>Red</option> # # => <select id="colors" multiple="multiple" name="colors[]"><option>Red</option>
# # <option>Green</option><option>Blue</option></select> # # <option>Green</option><option>Blue</option></select>
# #
# select_tag "locations", "<option>Home</option><option selected="selected">Work</option><option>Out</option>" # select_tag "locations", "<option>Home</option><option selected="selected">Work</option><option>Out</option>"
@ -70,14 +70,15 @@ module ActionView
# # <option>Out</option></select> # # <option>Out</option></select>
# #
# select_tag "access", "<option>Read</option><option>Write</option>", :multiple => true, :class => 'form_input' # select_tag "access", "<option>Read</option><option>Write</option>", :multiple => true, :class => 'form_input'
# # => <select class="form_input" id="access" multiple="multiple" name="access"><option>Read</option> # # => <select class="form_input" id="access" multiple="multiple" name="access[]"><option>Read</option>
# # <option>Write</option></select> # # <option>Write</option></select>
# #
# select_tag "destination", "<option>NYC</option><option>Paris</option><option>Rome</option>", :disabled => true # select_tag "destination", "<option>NYC</option><option>Paris</option><option>Rome</option>", :disabled => true
# # => <select disabled="disabled" id="destination" name="destination"><option>NYC</option> # # => <select disabled="disabled" id="destination" name="destination"><option>NYC</option>
# # <option>Paris</option><option>Rome</option></select> # # <option>Paris</option><option>Rome</option></select>
def select_tag(name, option_tags = nil, options = {}) def select_tag(name, option_tags = nil, options = {})
content_tag :select, option_tags, { "name" => name, "id" => name }.update(options.stringify_keys) html_name = (options[:multiple] == true && !name.to_s.ends_with?("[]")) ? "#{name}[]" : name
content_tag :select, option_tags, { "name" => html_name, "id" => name }.update(options.stringify_keys)
end end
# Creates a standard text field; use these text fields to input smaller chunks of text like a username # Creates a standard text field; use these text fields to input smaller chunks of text like a username
@ -155,10 +156,10 @@ module ActionView
# Creates a file upload field. If you are using file uploads then you will also need # Creates a file upload field. If you are using file uploads then you will also need
# to set the multipart option for the form tag: # to set the multipart option for the form tag:
# #
# <%= form_tag { :action => "post" }, { :multipart => true } %> # <% form_tag '/upload', :multipart => true do %>
# <label for="file">File to Upload</label> <%= file_field_tag "file" %> # <label for="file">File to Upload</label> <%= file_field_tag "file" %>
# <%= submit_tag %> # <%= submit_tag %>
# <%= end_form_tag %> # <% end %>
# #
# The specified URL will then be passed a File object containing the selected file, or if the field # The specified URL will then be passed a File object containing the selected file, or if the field
# was left blank, a StringIO object. # was left blank, a StringIO object.
@ -351,19 +352,16 @@ module ActionView
disable_with = "this.value='#{disable_with}'" disable_with = "this.value='#{disable_with}'"
disable_with << ";#{options.delete('onclick')}" if options['onclick'] disable_with << ";#{options.delete('onclick')}" if options['onclick']
options["onclick"] = [ options["onclick"] = "if (window.hiddenCommit) { window.hiddenCommit.setAttribute('value', this.value); }"
"this.setAttribute('originalValue', this.value)", options["onclick"] << "else { hiddenCommit = this.cloneNode(false);hiddenCommit.setAttribute('type', 'hidden');this.form.appendChild(hiddenCommit); }"
"this.disabled=true", options["onclick"] << "this.setAttribute('originalValue', this.value);this.disabled = true;#{disable_with};"
disable_with, options["onclick"] << "result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit());"
"result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit())", options["onclick"] << "if (result == false) { this.value = this.getAttribute('originalValue');this.disabled = false; }return result;"
"if (result == false) { this.value = this.getAttribute('originalValue'); this.disabled = false }",
"return result;",
].join(";")
end end
if confirm = options.delete("confirm") if confirm = options.delete("confirm")
options["onclick"] ||= '' options["onclick"] ||= ''
options["onclick"] += "return #{confirm_javascript_function(confirm)};" options["onclick"] << "return #{confirm_javascript_function(confirm)};"
end end
tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys) tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys)
@ -374,6 +372,9 @@ module ActionView
# <tt>source</tt> is passed to AssetTagHelper#image_path # <tt>source</tt> is passed to AssetTagHelper#image_path
# #
# ==== Options # ==== Options
# * <tt>:confirm => 'question?'</tt> - This will add a JavaScript confirm
# prompt with the question specified. If the user accepts, the form is
# processed normally, otherwise no action is taken.
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * Any other key creates standard HTML options for the tag. # * Any other key creates standard HTML options for the tag.
# #
@ -390,12 +391,20 @@ module ActionView
# image_submit_tag("agree.png", :disabled => true, :class => "agree-disagree-button") # image_submit_tag("agree.png", :disabled => true, :class => "agree-disagree-button")
# # => <input class="agree-disagree-button" disabled="disabled" src="/images/agree.png" type="image" /> # # => <input class="agree-disagree-button" disabled="disabled" src="/images/agree.png" type="image" />
def image_submit_tag(source, options = {}) def image_submit_tag(source, options = {})
options.stringify_keys!
if confirm = options.delete("confirm")
options["onclick"] ||= ''
options["onclick"] += "return #{confirm_javascript_function(confirm)};"
end
tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options.stringify_keys) tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options.stringify_keys)
end end
# Creates a field set for grouping HTML form elements. # Creates a field set for grouping HTML form elements.
# #
# <tt>legend</tt> will become the fieldset's title (optional as per W3C). # <tt>legend</tt> will become the fieldset's title (optional as per W3C).
# <tt>options</tt> accept the same values as tag.
# #
# === Examples # === Examples
# <% field_set_tag do %> # <% field_set_tag do %>
@ -407,12 +416,17 @@ module ActionView
# <p><%= text_field_tag 'name' %></p> # <p><%= text_field_tag 'name' %></p>
# <% end %> # <% end %>
# # => <fieldset><legend>Your details</legend><p><input id="name" name="name" type="text" /></p></fieldset> # # => <fieldset><legend>Your details</legend><p><input id="name" name="name" type="text" /></p></fieldset>
def field_set_tag(legend = nil, &block) #
# <% field_set_tag nil, :class => 'format' do %>
# <p><%= text_field_tag 'name' %></p>
# <% end %>
# # => <fieldset class="format"><p><input id="name" name="name" type="text" /></p></fieldset>
def field_set_tag(legend = nil, options = nil, &block)
content = capture(&block) content = capture(&block)
concat(tag(:fieldset, {}, true), block.binding) concat(tag(:fieldset, options, true))
concat(content_tag(:legend, legend), block.binding) unless legend.blank? concat(content_tag(:legend, legend)) unless legend.blank?
concat(content, block.binding) concat(content)
concat("</fieldset>", block.binding) concat("</fieldset>")
end end
private private
@ -444,9 +458,9 @@ module ActionView
def form_tag_in_block(html_options, &block) def form_tag_in_block(html_options, &block)
content = capture(&block) content = capture(&block)
concat(form_tag_html(html_options), block.binding) concat(form_tag_html(html_options))
concat(content, block.binding) concat(content)
concat("</form>", block.binding) concat("</form>")
end end
def token_tag def token_tag

View file

@ -44,13 +44,22 @@ module ActionView
include PrototypeHelper include PrototypeHelper
# Returns a link that will trigger a JavaScript +function+ using the # Returns a link of the given +name+ that will trigger a JavaScript +function+ using the
# onclick handler and return false after the fact. # onclick handler and return false after the fact.
# #
# The first argument +name+ is used as the link text.
#
# The next arguments are optional and may include the javascript function definition and a hash of html_options.
#
# The +function+ argument can be omitted in favor of an +update_page+ # The +function+ argument can be omitted in favor of an +update_page+
# block, which evaluates to a string when the template is rendered # block, which evaluates to a string when the template is rendered
# (instead of making an Ajax request first). # (instead of making an Ajax request first).
# #
# The +html_options+ will accept a hash of html attributes for the link tag. Some examples are :class => "nav_button", :id => "articles_nav_button"
#
# Note: if you choose to specify the javascript function in a block, but would like to pass html_options, set the +function+ parameter to nil
#
#
# Examples: # Examples:
# link_to_function "Greeting", "alert('Hello world!')" # link_to_function "Greeting", "alert('Hello world!')"
# Produces: # Produces:
@ -81,25 +90,29 @@ module ActionView
# #
def link_to_function(name, *args, &block) def link_to_function(name, *args, &block)
html_options = args.extract_options!.symbolize_keys html_options = args.extract_options!.symbolize_keys
function = args[0] || ''
function = update_page(&block) if block_given? function = block_given? ? update_page(&block) : args[0] || ''
content_tag( onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function}; return false;"
"a", name, href = html_options[:href] || '#'
html_options.merge({
:href => html_options[:href] || "#", content_tag(:a, name, html_options.merge(:href => href, :onclick => onclick))
:onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function}; return false;"
})
)
end end
# Returns a button that'll trigger a JavaScript +function+ using the # Returns a button with the given +name+ text that'll trigger a JavaScript +function+ using the
# onclick handler. # onclick handler.
# #
# The first argument +name+ is used as the button's value or display text.
#
# The next arguments are optional and may include the javascript function definition and a hash of html_options.
#
# The +function+ argument can be omitted in favor of an +update_page+ # The +function+ argument can be omitted in favor of an +update_page+
# block, which evaluates to a string when the template is rendered # block, which evaluates to a string when the template is rendered
# (instead of making an Ajax request first). # (instead of making an Ajax request first).
# #
# The +html_options+ will accept a hash of html attributes for the link tag. Some examples are :class => "nav_button", :id => "articles_nav_button"
#
# Note: if you choose to specify the javascript function in a block, but would like to pass html_options, set the +function+ parameter to nil
#
# Examples: # Examples:
# button_to_function "Greeting", "alert('Hello world!')" # button_to_function "Greeting", "alert('Hello world!')"
# button_to_function "Delete", "if (confirm('Really?')) do_delete()" # button_to_function "Delete", "if (confirm('Really?')) do_delete()"
@ -111,45 +124,29 @@ module ActionView
# end # end
def button_to_function(name, *args, &block) def button_to_function(name, *args, &block)
html_options = args.extract_options!.symbolize_keys html_options = args.extract_options!.symbolize_keys
function = args[0] || ''
function = update_page(&block) if block_given? function = block_given? ? update_page(&block) : args[0] || ''
tag(:input, html_options.merge({ onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function};"
:type => "button", :value => name,
:onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" tag(:input, html_options.merge(:type => 'button', :value => name, :onclick => onclick))
}))
end end
# Includes the Action Pack JavaScript libraries inside a single <script> JS_ESCAPE_MAP = {
# tag. The function first includes prototype.js and then its core extensions, '\\' => '\\\\',
# (determined by filenames starting with "prototype"). '</' => '<\/',
# Afterwards, any additional scripts will be included in undefined order. "\r\n" => '\n',
# "\n" => '\n',
# Note: The recommended approach is to copy the contents of "\r" => '\n',
# lib/action_view/helpers/javascripts/ into your application's '"' => '\\"',
# public/javascripts/ directory, and use +javascript_include_tag+ to "'" => "\\'" }
# create remote <script> links.
def define_javascript_functions
javascript = "<script type=\"#{Mime::JS}\">"
# load prototype.js and its extensions first
prototype_libs = Dir.glob(File.join(JAVASCRIPT_PATH, 'prototype*')).sort.reverse
prototype_libs.each do |filename|
javascript << "\n" << IO.read(filename)
end
# load other libraries
(Dir.glob(File.join(JAVASCRIPT_PATH, '*')) - prototype_libs).each do |filename|
javascript << "\n" << IO.read(filename)
end
javascript << '</script>'
end
deprecate :define_javascript_functions=>"use javascript_include_tag instead"
# Escape carrier returns and single and double quotes for JavaScript segments. # Escape carrier returns and single and double quotes for JavaScript segments.
def escape_javascript(javascript) def escape_javascript(javascript)
(javascript || '').gsub('\\','\0\0').gsub('</','<\/').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" } if javascript
javascript.gsub(/(\\|<\/|\r\n|[\n\r"'])/) { JS_ESCAPE_MAP[$1] }
else
''
end
end end
# Returns a JavaScript tag with the +content+ inside. Example: # Returns a JavaScript tag with the +content+ inside. Example:
@ -172,19 +169,20 @@ module ActionView
# alert('All is good') # alert('All is good')
# <% end -%> # <% end -%>
def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block) def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block)
content =
if block_given? if block_given?
html_options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash) html_options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
content = capture(&block) capture(&block)
else else
content = content_or_options_with_block content_or_options_with_block
end end
javascript_tag = content_tag("script", javascript_cdata_section(content), html_options.merge(:type => Mime::JS)) tag = content_tag(:script, javascript_cdata_section(content), html_options.merge(:type => Mime::JS))
if block_given? && block_is_within_action_view?(block) if block_called_from_erb?(block)
concat(javascript_tag, block.binding) concat(tag)
else else
javascript_tag tag
end end
end end
@ -194,24 +192,20 @@ module ActionView
protected protected
def options_for_javascript(options) def options_for_javascript(options)
'{' + options.map {|k, v| "#{k}:#{v}"}.sort.join(', ') + '}' if options.empty?
'{}'
else
"{#{options.keys.map { |k| "#{k}:#{options[k]}" }.sort.join(', ')}}"
end
end end
def array_or_string_for_javascript(option) def array_or_string_for_javascript(option)
js_option = if option.kind_of?(Array) if option.kind_of?(Array)
"['#{option.join('\',\'')}']" "['#{option.join('\',\'')}']"
elsif !option.nil? elsif !option.nil?
"'#{option}'" "'#{option}'"
end end
js_option
end
private
def block_is_within_action_view?(block)
eval("defined? _erbout", block.binding)
end end
end end
JavascriptHelper = JavaScriptHelper unless const_defined? :JavascriptHelper
end end
end end

View file

@ -1,5 +1,5 @@
/* Prototype JavaScript framework, version 1.6.0.1 /* Prototype JavaScript framework, version 1.6.0.2
* (c) 2005-2007 Sam Stephenson * (c) 2005-2008 Sam Stephenson
* *
* Prototype is freely distributable under the terms of an MIT-style license. * Prototype is freely distributable under the terms of an MIT-style license.
* For details, see the Prototype web site: http://www.prototypejs.org/ * For details, see the Prototype web site: http://www.prototypejs.org/
@ -7,7 +7,7 @@
*--------------------------------------------------------------------------*/ *--------------------------------------------------------------------------*/
var Prototype = { var Prototype = {
Version: '1.6.0.1', Version: '1.6.0.2',
Browser: { Browser: {
IE: !!(window.attachEvent && !window.opera), IE: !!(window.attachEvent && !window.opera),
@ -110,7 +110,7 @@ Object.extend(Object, {
try { try {
if (Object.isUndefined(object)) return 'undefined'; if (Object.isUndefined(object)) return 'undefined';
if (object === null) return 'null'; if (object === null) return 'null';
return object.inspect ? object.inspect() : object.toString(); return object.inspect ? object.inspect() : String(object);
} catch (e) { } catch (e) {
if (e instanceof RangeError) return '...'; if (e instanceof RangeError) return '...';
throw e; throw e;
@ -171,7 +171,8 @@ Object.extend(Object, {
}, },
isArray: function(object) { isArray: function(object) {
return object && object.constructor === Array; return object != null && typeof object == "object" &&
'splice' in object && 'join' in object;
}, },
isHash: function(object) { isHash: function(object) {
@ -578,7 +579,7 @@ var Template = Class.create({
} }
return before + String.interpret(ctx); return before + String.interpret(ctx);
}.bind(this)); });
} }
}); });
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
@ -806,20 +807,20 @@ Object.extend(Enumerable, {
function $A(iterable) { function $A(iterable) {
if (!iterable) return []; if (!iterable) return [];
if (iterable.toArray) return iterable.toArray(); if (iterable.toArray) return iterable.toArray();
var length = iterable.length, results = new Array(length); var length = iterable.length || 0, results = new Array(length);
while (length--) results[length] = iterable[length]; while (length--) results[length] = iterable[length];
return results; return results;
} }
if (Prototype.Browser.WebKit) { if (Prototype.Browser.WebKit) {
function $A(iterable) { $A = function(iterable) {
if (!iterable) return []; if (!iterable) return [];
if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') && if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') &&
iterable.toArray) return iterable.toArray(); iterable.toArray) return iterable.toArray();
var length = iterable.length, results = new Array(length); var length = iterable.length || 0, results = new Array(length);
while (length--) results[length] = iterable[length]; while (length--) results[length] = iterable[length];
return results; return results;
} };
} }
Array.from = $A; Array.from = $A;
@ -1298,7 +1299,7 @@ Ajax.Request = Class.create(Ajax.Base, {
var contentType = response.getHeader('Content-type'); var contentType = response.getHeader('Content-type');
if (this.options.evalJS == 'force' if (this.options.evalJS == 'force'
|| (this.options.evalJS && contentType || (this.options.evalJS && this.isSameOrigin() && contentType
&& contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
this.evalResponse(); this.evalResponse();
} }
@ -1316,9 +1317,18 @@ Ajax.Request = Class.create(Ajax.Base, {
} }
}, },
isSameOrigin: function() {
var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
protocol: location.protocol,
domain: document.domain,
port: location.port ? ':' + location.port : ''
}));
},
getHeader: function(name) { getHeader: function(name) {
try { try {
return this.transport.getResponseHeader(name); return this.transport.getResponseHeader(name) || null;
} catch (e) { return null } } catch (e) { return null }
}, },
@ -1391,7 +1401,8 @@ Ajax.Response = Class.create({
if (!json) return null; if (!json) return null;
json = decodeURIComponent(escape(json)); json = decodeURIComponent(escape(json));
try { try {
return json.evalJSON(this.request.options.sanitizeJSON); return json.evalJSON(this.request.options.sanitizeJSON ||
!this.request.isSameOrigin());
} catch (e) { } catch (e) {
this.request.dispatchException(e); this.request.dispatchException(e);
} }
@ -1404,7 +1415,8 @@ Ajax.Response = Class.create({
this.responseText.blank()) this.responseText.blank())
return null; return null;
try { try {
return this.responseText.evalJSON(options.sanitizeJSON); return this.responseText.evalJSON(options.sanitizeJSON ||
!this.request.isSameOrigin());
} catch (e) { } catch (e) {
this.request.dispatchException(e); this.request.dispatchException(e);
} }
@ -1608,24 +1620,28 @@ Element.Methods = {
Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
insertions = {bottom:insertions}; insertions = {bottom:insertions};
var content, t, range; var content, insert, tagName, childNodes;
for (position in insertions) { for (var position in insertions) {
content = insertions[position]; content = insertions[position];
position = position.toLowerCase(); position = position.toLowerCase();
t = Element._insertionTranslations[position]; insert = Element._insertionTranslations[position];
if (content && content.toElement) content = content.toElement(); if (content && content.toElement) content = content.toElement();
if (Object.isElement(content)) { if (Object.isElement(content)) {
t.insert(element, content); insert(element, content);
continue; continue;
} }
content = Object.toHTML(content); content = Object.toHTML(content);
range = element.ownerDocument.createRange(); tagName = ((position == 'before' || position == 'after')
t.initializeRange(element, range); ? element.parentNode : element).tagName.toUpperCase();
t.insert(element, range.createContextualFragment(content.stripScripts()));
childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
if (position == 'top' || position == 'after') childNodes.reverse();
childNodes.each(insert.curry(element));
content.evalScripts.bind(content).defer(); content.evalScripts.bind(content).defer();
} }
@ -1670,7 +1686,7 @@ Element.Methods = {
}, },
descendants: function(element) { descendants: function(element) {
return $(element).getElementsBySelector("*"); return $(element).select("*");
}, },
firstDescendant: function(element) { firstDescendant: function(element) {
@ -1709,32 +1725,31 @@ Element.Methods = {
element = $(element); element = $(element);
if (arguments.length == 1) return $(element.parentNode); if (arguments.length == 1) return $(element.parentNode);
var ancestors = element.ancestors(); var ancestors = element.ancestors();
return expression ? Selector.findElement(ancestors, expression, index) : return Object.isNumber(expression) ? ancestors[expression] :
ancestors[index || 0]; Selector.findElement(ancestors, expression, index);
}, },
down: function(element, expression, index) { down: function(element, expression, index) {
element = $(element); element = $(element);
if (arguments.length == 1) return element.firstDescendant(); if (arguments.length == 1) return element.firstDescendant();
var descendants = element.descendants(); return Object.isNumber(expression) ? element.descendants()[expression] :
return expression ? Selector.findElement(descendants, expression, index) : element.select(expression)[index || 0];
descendants[index || 0];
}, },
previous: function(element, expression, index) { previous: function(element, expression, index) {
element = $(element); element = $(element);
if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
var previousSiblings = element.previousSiblings(); var previousSiblings = element.previousSiblings();
return expression ? Selector.findElement(previousSiblings, expression, index) : return Object.isNumber(expression) ? previousSiblings[expression] :
previousSiblings[index || 0]; Selector.findElement(previousSiblings, expression, index);
}, },
next: function(element, expression, index) { next: function(element, expression, index) {
element = $(element); element = $(element);
if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
var nextSiblings = element.nextSiblings(); var nextSiblings = element.nextSiblings();
return expression ? Selector.findElement(nextSiblings, expression, index) : return Object.isNumber(expression) ? nextSiblings[expression] :
nextSiblings[index || 0]; Selector.findElement(nextSiblings, expression, index);
}, },
select: function() { select: function() {
@ -1860,7 +1875,8 @@ Element.Methods = {
do { ancestor = ancestor.parentNode; } do { ancestor = ancestor.parentNode; }
while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode); while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode);
} }
if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex); if (nextAncestor && nextAncestor.sourceIndex)
return (e > a && e < nextAncestor.sourceIndex);
} }
while (element = element.parentNode) while (element = element.parentNode)
@ -2004,7 +2020,7 @@ Element.Methods = {
if (element) { if (element) {
if (element.tagName == 'BODY') break; if (element.tagName == 'BODY') break;
var p = Element.getStyle(element, 'position'); var p = Element.getStyle(element, 'position');
if (p == 'relative' || p == 'absolute') break; if (p !== 'static') break;
} }
} while (element); } while (element);
return Element._returnOffset(valueL, valueT); return Element._returnOffset(valueL, valueT);
@ -2153,46 +2169,6 @@ Element._attributeTranslations = {
} }
}; };
if (!document.createRange || Prototype.Browser.Opera) {
Element.Methods.insert = function(element, insertions) {
element = $(element);
if (Object.isString(insertions) || Object.isNumber(insertions) ||
Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
insertions = { bottom: insertions };
var t = Element._insertionTranslations, content, position, pos, tagName;
for (position in insertions) {
content = insertions[position];
position = position.toLowerCase();
pos = t[position];
if (content && content.toElement) content = content.toElement();
if (Object.isElement(content)) {
pos.insert(element, content);
continue;
}
content = Object.toHTML(content);
tagName = ((position == 'before' || position == 'after')
? element.parentNode : element).tagName.toUpperCase();
if (t.tags[tagName]) {
var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
if (position == 'top' || position == 'after') fragments.reverse();
fragments.each(pos.insert.curry(element));
}
else element.insertAdjacentHTML(pos.adjacency, content.stripScripts());
content.evalScripts.bind(content).defer();
}
return element;
};
}
if (Prototype.Browser.Opera) { if (Prototype.Browser.Opera) {
Element.Methods.getStyle = Element.Methods.getStyle.wrap( Element.Methods.getStyle = Element.Methods.getStyle.wrap(
function(proceed, element, style) { function(proceed, element, style) {
@ -2237,12 +2213,31 @@ if (Prototype.Browser.Opera) {
} }
else if (Prototype.Browser.IE) { else if (Prototype.Browser.IE) {
$w('positionedOffset getOffsetParent viewportOffset').each(function(method) { // IE doesn't report offsets correctly for static elements, so we change them
// to "relative" to get the values, then change them back.
Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
function(proceed, element) {
element = $(element);
var position = element.getStyle('position');
if (position !== 'static') return proceed(element);
element.setStyle({ position: 'relative' });
var value = proceed(element);
element.setStyle({ position: position });
return value;
}
);
$w('positionedOffset viewportOffset').each(function(method) {
Element.Methods[method] = Element.Methods[method].wrap( Element.Methods[method] = Element.Methods[method].wrap(
function(proceed, element) { function(proceed, element) {
element = $(element); element = $(element);
var position = element.getStyle('position'); var position = element.getStyle('position');
if (position != 'static') return proceed(element); if (position !== 'static') return proceed(element);
// Trigger hasLayout on the offset parent so that IE6 reports
// accurate offsetTop and offsetLeft values for position: fixed.
var offsetParent = element.getOffsetParent();
if (offsetParent && offsetParent.getStyle('position') === 'fixed')
offsetParent.setStyle({ zoom: 1 });
element.setStyle({ position: 'relative' }); element.setStyle({ position: 'relative' });
var value = proceed(element); var value = proceed(element);
element.setStyle({ position: position }); element.setStyle({ position: position });
@ -2324,7 +2319,10 @@ else if (Prototype.Browser.IE) {
}; };
Element._attributeTranslations.write = { Element._attributeTranslations.write = {
names: Object.clone(Element._attributeTranslations.read.names), names: Object.extend({
cellpadding: 'cellPadding',
cellspacing: 'cellSpacing'
}, Element._attributeTranslations.read.names),
values: { values: {
checked: function(element, value) { checked: function(element, value) {
element.checked = !!value; element.checked = !!value;
@ -2444,7 +2442,7 @@ if (Prototype.Browser.IE || Prototype.Browser.Opera) {
}; };
} }
if (document.createElement('div').outerHTML) { if ('outerHTML' in document.createElement('div')) {
Element.Methods.replace = function(element, content) { Element.Methods.replace = function(element, content) {
element = $(element); element = $(element);
@ -2482,46 +2480,26 @@ Element._returnOffset = function(l, t) {
Element._getContentFromAnonymousElement = function(tagName, html) { Element._getContentFromAnonymousElement = function(tagName, html) {
var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
if (t) {
div.innerHTML = t[0] + html + t[1]; div.innerHTML = t[0] + html + t[1];
t[2].times(function() { div = div.firstChild }); t[2].times(function() { div = div.firstChild });
} else div.innerHTML = html;
return $A(div.childNodes); return $A(div.childNodes);
}; };
Element._insertionTranslations = { Element._insertionTranslations = {
before: { before: function(element, node) {
adjacency: 'beforeBegin',
insert: function(element, node) {
element.parentNode.insertBefore(node, element); element.parentNode.insertBefore(node, element);
}, },
initializeRange: function(element, range) { top: function(element, node) {
range.setStartBefore(element);
}
},
top: {
adjacency: 'afterBegin',
insert: function(element, node) {
element.insertBefore(node, element.firstChild); element.insertBefore(node, element.firstChild);
}, },
initializeRange: function(element, range) { bottom: function(element, node) {
range.selectNodeContents(element);
range.collapse(true);
}
},
bottom: {
adjacency: 'beforeEnd',
insert: function(element, node) {
element.appendChild(node); element.appendChild(node);
}
}, },
after: { after: function(element, node) {
adjacency: 'afterEnd',
insert: function(element, node) {
element.parentNode.insertBefore(node, element.nextSibling); element.parentNode.insertBefore(node, element.nextSibling);
}, },
initializeRange: function(element, range) {
range.setStartAfter(element);
}
},
tags: { tags: {
TABLE: ['<table>', '</table>', 1], TABLE: ['<table>', '</table>', 1],
TBODY: ['<table><tbody>', '</tbody></table>', 2], TBODY: ['<table><tbody>', '</tbody></table>', 2],
@ -2532,7 +2510,6 @@ Element._insertionTranslations = {
}; };
(function() { (function() {
this.bottom.initializeRange = this.top.initializeRange;
Object.extend(this.tags, { Object.extend(this.tags, {
THEAD: this.tags.TBODY, THEAD: this.tags.TBODY,
TFOOT: this.tags.TBODY, TFOOT: this.tags.TBODY,
@ -2716,7 +2693,7 @@ document.viewport = {
window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
} }
}; };
/* Portions of the Selector class are derived from Jack Slocums DomQuery, /* Portions of the Selector class are derived from Jack Slocumâs DomQuery,
* part of YUI-Ext version 0.40, distributed under the terms of an MIT-style * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
* license. Please see http://www.yui-ext.com/ for more information. */ * license. Please see http://www.yui-ext.com/ for more information. */
@ -2962,10 +2939,10 @@ Object.extend(Selector, {
tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;',
className: 'n = h.className(n, r, "#{1}", c); c = false;', className: 'n = h.className(n, r, "#{1}", c); c = false;',
id: 'n = h.id(n, r, "#{1}", c); c = false;', id: 'n = h.id(n, r, "#{1}", c); c = false;',
attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;', attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
attr: function(m) { attr: function(m) {
m[3] = (m[5] || m[6]); m[3] = (m[5] || m[6]);
return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m); return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
}, },
pseudo: function(m) { pseudo: function(m) {
if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
@ -2989,7 +2966,8 @@ Object.extend(Selector, {
tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, tagName: /^\s*(\*|[\w\-]+)(\b|$)?/,
id: /^#([\w\-\*]+)(\b|$)/, id: /^#([\w\-\*]+)(\b|$)/,
className: /^\.([\w\-\*]+)(\b|$)/, className: /^\.([\w\-\*]+)(\b|$)/,
pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/, pseudo:
/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
attrPresence: /^\[([\w]+)\]/, attrPresence: /^\[([\w]+)\]/,
attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
}, },
@ -3014,7 +2992,7 @@ Object.extend(Selector, {
attr: function(element, matches) { attr: function(element, matches) {
var nodeValue = Element.readAttribute(element, matches[1]); var nodeValue = Element.readAttribute(element, matches[1]);
return Selector.operators[matches[2]](nodeValue, matches[3]); return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
} }
}, },
@ -3029,14 +3007,15 @@ Object.extend(Selector, {
// marks an array of nodes for counting // marks an array of nodes for counting
mark: function(nodes) { mark: function(nodes) {
var _true = Prototype.emptyFunction;
for (var i = 0, node; node = nodes[i]; i++) for (var i = 0, node; node = nodes[i]; i++)
node._counted = true; node._countedByPrototype = _true;
return nodes; return nodes;
}, },
unmark: function(nodes) { unmark: function(nodes) {
for (var i = 0, node; node = nodes[i]; i++) for (var i = 0, node; node = nodes[i]; i++)
node._counted = undefined; node._countedByPrototype = undefined;
return nodes; return nodes;
}, },
@ -3044,15 +3023,15 @@ Object.extend(Selector, {
// "ofType" flag indicates whether we're indexing for nth-of-type // "ofType" flag indicates whether we're indexing for nth-of-type
// rather than nth-child // rather than nth-child
index: function(parentNode, reverse, ofType) { index: function(parentNode, reverse, ofType) {
parentNode._counted = true; parentNode._countedByPrototype = Prototype.emptyFunction;
if (reverse) { if (reverse) {
for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
var node = nodes[i]; var node = nodes[i];
if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
} }
} else { } else {
for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
} }
}, },
@ -3061,8 +3040,8 @@ Object.extend(Selector, {
if (nodes.length == 0) return nodes; if (nodes.length == 0) return nodes;
var results = [], n; var results = [], n;
for (var i = 0, l = nodes.length; i < l; i++) for (var i = 0, l = nodes.length; i < l; i++)
if (!(n = nodes[i])._counted) { if (!(n = nodes[i])._countedByPrototype) {
n._counted = true; n._countedByPrototype = Prototype.emptyFunction;
results.push(Element.extend(n)); results.push(Element.extend(n));
} }
return Selector.handlers.unmark(results); return Selector.handlers.unmark(results);
@ -3114,7 +3093,7 @@ Object.extend(Selector, {
// TOKEN FUNCTIONS // TOKEN FUNCTIONS
tagName: function(nodes, root, tagName, combinator) { tagName: function(nodes, root, tagName, combinator) {
tagName = tagName.toUpperCase(); var uTagName = tagName.toUpperCase();
var results = [], h = Selector.handlers; var results = [], h = Selector.handlers;
if (nodes) { if (nodes) {
if (combinator) { if (combinator) {
@ -3127,7 +3106,7 @@ Object.extend(Selector, {
if (tagName == "*") return nodes; if (tagName == "*") return nodes;
} }
for (var i = 0, node; node = nodes[i]; i++) for (var i = 0, node; node = nodes[i]; i++)
if (node.tagName.toUpperCase() == tagName) results.push(node); if (node.tagName.toUpperCase() === uTagName) results.push(node);
return results; return results;
} else return root.getElementsByTagName(tagName); } else return root.getElementsByTagName(tagName);
}, },
@ -3174,16 +3153,18 @@ Object.extend(Selector, {
return results; return results;
}, },
attrPresence: function(nodes, root, attr) { attrPresence: function(nodes, root, attr, combinator) {
if (!nodes) nodes = root.getElementsByTagName("*"); if (!nodes) nodes = root.getElementsByTagName("*");
if (nodes && combinator) nodes = this[combinator](nodes);
var results = []; var results = [];
for (var i = 0, node; node = nodes[i]; i++) for (var i = 0, node; node = nodes[i]; i++)
if (Element.hasAttribute(node, attr)) results.push(node); if (Element.hasAttribute(node, attr)) results.push(node);
return results; return results;
}, },
attr: function(nodes, root, attr, value, operator) { attr: function(nodes, root, attr, value, operator, combinator) {
if (!nodes) nodes = root.getElementsByTagName("*"); if (!nodes) nodes = root.getElementsByTagName("*");
if (nodes && combinator) nodes = this[combinator](nodes);
var handler = Selector.operators[operator], results = []; var handler = Selector.operators[operator], results = [];
for (var i = 0, node; node = nodes[i]; i++) { for (var i = 0, node; node = nodes[i]; i++) {
var nodeValue = Element.readAttribute(node, attr); var nodeValue = Element.readAttribute(node, attr);
@ -3262,7 +3243,7 @@ Object.extend(Selector, {
var h = Selector.handlers, results = [], indexed = [], m; var h = Selector.handlers, results = [], indexed = [], m;
h.mark(nodes); h.mark(nodes);
for (var i = 0, node; node = nodes[i]; i++) { for (var i = 0, node; node = nodes[i]; i++) {
if (!node.parentNode._counted) { if (!node.parentNode._countedByPrototype) {
h.index(node.parentNode, reverse, ofType); h.index(node.parentNode, reverse, ofType);
indexed.push(node.parentNode); indexed.push(node.parentNode);
} }
@ -3300,7 +3281,7 @@ Object.extend(Selector, {
var exclusions = new Selector(selector).findElements(root); var exclusions = new Selector(selector).findElements(root);
h.mark(exclusions); h.mark(exclusions);
for (var i = 0, results = [], node; node = nodes[i]; i++) for (var i = 0, results = [], node; node = nodes[i]; i++)
if (!node._counted) results.push(node); if (!node._countedByPrototype) results.push(node);
h.unmark(exclusions); h.unmark(exclusions);
return results; return results;
}, },
@ -3334,11 +3315,19 @@ Object.extend(Selector, {
'|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); } '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
}, },
split: function(expression) {
var expressions = [];
expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
expressions.push(m[1].strip());
});
return expressions;
},
matchElements: function(elements, expression) { matchElements: function(elements, expression) {
var matches = new Selector(expression).findElements(), h = Selector.handlers; var matches = $$(expression), h = Selector.handlers;
h.mark(matches); h.mark(matches);
for (var i = 0, results = [], element; element = elements[i]; i++) for (var i = 0, results = [], element; element = elements[i]; i++)
if (element._counted) results.push(element); if (element._countedByPrototype) results.push(element);
h.unmark(matches); h.unmark(matches);
return results; return results;
}, },
@ -3351,11 +3340,7 @@ Object.extend(Selector, {
}, },
findChildElements: function(element, expressions) { findChildElements: function(element, expressions) {
var exprs = expressions.join(','); expressions = Selector.split(expressions.join(','));
expressions = [];
exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
expressions.push(m[1].strip());
});
var results = [], h = Selector.handlers; var results = [], h = Selector.handlers;
for (var i = 0, l = expressions.length, selector; i < l; i++) { for (var i = 0, l = expressions.length, selector; i < l; i++) {
selector = new Selector(expressions[i].strip()); selector = new Selector(expressions[i].strip());
@ -3366,13 +3351,22 @@ Object.extend(Selector, {
}); });
if (Prototype.Browser.IE) { if (Prototype.Browser.IE) {
Object.extend(Selector.handlers, {
// IE returns comment nodes on getElementsByTagName("*"). // IE returns comment nodes on getElementsByTagName("*").
// Filter them out. // Filter them out.
Selector.handlers.concat = function(a, b) { concat: function(a, b) {
for (var i = 0, node; node = b[i]; i++) for (var i = 0, node; node = b[i]; i++)
if (node.tagName !== "!") a.push(node); if (node.tagName !== "!") a.push(node);
return a; return a;
}; },
// IE improperly serializes _countedByPrototype in (inner|outer)HTML.
unmark: function(nodes) {
for (var i = 0, node; node = nodes[i]; i++)
node.removeAttribute('_countedByPrototype');
return nodes;
}
});
} }
function $$() { function $$() {
@ -3850,9 +3844,9 @@ Object.extend(Event, (function() {
var cache = Event.cache; var cache = Event.cache;
function getEventID(element) { function getEventID(element) {
if (element._eventID) return element._eventID; if (element._prototypeEventID) return element._prototypeEventID[0];
arguments.callee.id = arguments.callee.id || 1; arguments.callee.id = arguments.callee.id || 1;
return element._eventID = ++arguments.callee.id; return element._prototypeEventID = [++arguments.callee.id];
} }
function getDOMEventName(eventName) { function getDOMEventName(eventName) {
@ -3880,7 +3874,7 @@ Object.extend(Event, (function() {
return false; return false;
Event.extend(event); Event.extend(event);
handler.call(element, event) handler.call(element, event);
}; };
wrapper.handler = handler; wrapper.handler = handler;
@ -3962,11 +3956,12 @@ Object.extend(Event, (function() {
if (element == document && document.createEvent && !element.dispatchEvent) if (element == document && document.createEvent && !element.dispatchEvent)
element = document.documentElement; element = document.documentElement;
var event;
if (document.createEvent) { if (document.createEvent) {
var event = document.createEvent("HTMLEvents"); event = document.createEvent("HTMLEvents");
event.initEvent("dataavailable", true, true); event.initEvent("dataavailable", true, true);
} else { } else {
var event = document.createEventObject(); event = document.createEventObject();
event.eventType = "ondataavailable"; event.eventType = "ondataavailable";
} }
@ -3995,20 +3990,21 @@ Element.addMethods({
Object.extend(document, { Object.extend(document, {
fire: Element.Methods.fire.methodize(), fire: Element.Methods.fire.methodize(),
observe: Element.Methods.observe.methodize(), observe: Element.Methods.observe.methodize(),
stopObserving: Element.Methods.stopObserving.methodize() stopObserving: Element.Methods.stopObserving.methodize(),
loaded: false
}); });
(function() { (function() {
/* Support for the DOMContentLoaded event is based on work by Dan Webb, /* Support for the DOMContentLoaded event is based on work by Dan Webb,
Matthias Miller, Dean Edwards and John Resig. */ Matthias Miller, Dean Edwards and John Resig. */
var timer, fired = false; var timer;
function fireContentLoadedEvent() { function fireContentLoadedEvent() {
if (fired) return; if (document.loaded) return;
if (timer) window.clearInterval(timer); if (timer) window.clearInterval(timer);
document.fire("dom:loaded"); document.fire("dom:loaded");
fired = true; document.loaded = true;
} }
if (document.addEventListener) { if (document.addEventListener) {

View file

@ -25,11 +25,11 @@ module ActionView
# => +1.123.555.1234 x 1343 # => +1.123.555.1234 x 1343
def number_to_phone(number, options = {}) def number_to_phone(number, options = {})
number = number.to_s.strip unless number.nil? number = number.to_s.strip unless number.nil?
options = options.stringify_keys options = options.symbolize_keys
area_code = options["area_code"] || nil area_code = options[:area_code] || nil
delimiter = options["delimiter"] || "-" delimiter = options[:delimiter] || "-"
extension = options["extension"].to_s.strip || nil extension = options[:extension].to_s.strip || nil
country_code = options["country_code"] || nil country_code = options[:country_code] || nil
begin begin
str = "" str = ""
@ -69,16 +69,25 @@ module ActionView
# number_to_currency(1234567890.50, :unit => "&pound;", :separator => ",", :delimiter => "", :format => "%n %u") # number_to_currency(1234567890.50, :unit => "&pound;", :separator => ",", :delimiter => "", :format => "%n %u")
# # => 1234567890,50 &pound; # # => 1234567890,50 &pound;
def number_to_currency(number, options = {}) def number_to_currency(number, options = {})
options = options.stringify_keys options.symbolize_keys!
precision = options["precision"] || 2
unit = options["unit"] || "$" defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {}
separator = precision > 0 ? options["separator"] || "." : "" currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :raise => true) rescue {}
delimiter = options["delimiter"] || "," defaults = defaults.merge(currency)
format = options["format"] || "%u%n"
precision = options[:precision] || defaults[:precision]
unit = options[:unit] || defaults[:unit]
separator = options[:separator] || defaults[:separator]
delimiter = options[:delimiter] || defaults[:delimiter]
format = options[:format] || defaults[:format]
separator = '' if precision == 0
begin begin
parts = number_with_precision(number, precision).split('.') format.gsub(/%n/, number_with_precision(number,
format.gsub(/%n/, number_with_delimiter(parts[0], delimiter) + separator + parts[1].to_s).gsub(/%u/, unit) :precision => precision,
:delimiter => delimiter,
:separator => separator)
).gsub(/%u/, unit)
rescue rescue
number number
end end
@ -90,74 +99,139 @@ module ActionView
# ==== Options # ==== Options
# * <tt>:precision</tt> - Sets the level of precision (defaults to 3). # * <tt>:precision</tt> - Sets the level of precision (defaults to 3).
# * <tt>:separator</tt> - Sets the separator between the units (defaults to "."). # * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to "").
# #
# ==== Examples # ==== Examples
# number_to_percentage(100) # => 100.000% # number_to_percentage(100) # => 100.000%
# number_to_percentage(100, :precision => 0) # => 100% # number_to_percentage(100, :precision => 0) # => 100%
# # number_to_percentage(1000, :delimiter => '.', :separator => ',') # => 1.000,000%
# number_to_percentage(302.24398923423, :precision => 5) # number_to_percentage(302.24398923423, :precision => 5) # => 302.24399%
# # => 302.24399%
def number_to_percentage(number, options = {}) def number_to_percentage(number, options = {})
options = options.stringify_keys options.symbolize_keys!
precision = options["precision"] || 3
separator = options["separator"] || "." defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {}
percentage = I18n.translate(:'number.percentage.format', :locale => options[:locale], :raise => true) rescue {}
defaults = defaults.merge(percentage)
precision = options[:precision] || defaults[:precision]
separator = options[:separator] || defaults[:separator]
delimiter = options[:delimiter] || defaults[:delimiter]
begin begin
number = number_with_precision(number, precision) number_with_precision(number,
parts = number.split('.') :precision => precision,
if parts.at(1).nil? :separator => separator,
parts[0] + "%" :delimiter => delimiter) + "%"
else
parts[0] + separator + parts[1].to_s + "%"
end
rescue rescue
number number
end end
end end
# Formats a +number+ with grouped thousands using +delimiter+ (e.g., 12,324). You # Formats a +number+ with grouped thousands using +delimiter+ (e.g., 12,324). You can
# can customize the format using optional <em>delimiter</em> and <em>separator</em> parameters. # customize the format in the +options+ hash.
# #
# ==== Options # ==== Options
# * <tt>delimiter</tt> - Sets the thousands delimiter (defaults to ","). # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ",").
# * <tt>separator</tt> - Sets the separator between the units (defaults to "."). # * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
# #
# ==== Examples # ==== Examples
# number_with_delimiter(12345678) # => 12,345,678 # number_with_delimiter(12345678) # => 12,345,678
# number_with_delimiter(12345678.05) # => 12,345,678.05 # number_with_delimiter(12345678.05) # => 12,345,678.05
# number_with_delimiter(12345678, ".") # => 12.345.678 # number_with_delimiter(12345678, :delimiter => ".") # => 12.345.678
# # number_with_delimiter(12345678, :seperator => ",") # => 12,345,678
# number_with_delimiter(98765432.98, " ", ",") # number_with_delimiter(98765432.98, :delimiter => " ", :separator => ",")
# # => 98 765 432,98 # # => 98 765 432,98
def number_with_delimiter(number, delimiter=",", separator=".") #
# You can still use <tt>number_with_delimiter</tt> with the old API that accepts the
# +delimiter+ as its optional second and the +separator+ as its
# optional third parameter:
# number_with_delimiter(12345678, " ") # => 12 345.678
# number_with_delimiter(12345678.05, ".", ",") # => 12.345.678,05
def number_with_delimiter(number, *args)
options = args.extract_options!
options.symbolize_keys!
defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {}
unless args.empty?
ActiveSupport::Deprecation.warn('number_with_delimiter takes an option hash ' +
'instead of separate delimiter and precision arguments.', caller)
delimiter = args[0] || defaults[:delimiter]
separator = args[1] || defaults[:separator]
end
delimiter ||= (options[:delimiter] || defaults[:delimiter])
separator ||= (options[:separator] || defaults[:separator])
begin begin
parts = number.to_s.split('.') parts = number.to_s.split('.')
parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}") parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
parts.join separator parts.join(separator)
rescue rescue
number number
end end
end end
# Formats a +number+ with the specified level of +precision+ (e.g., 112.32 has a precision of 2). The default # Formats a +number+ with the specified level of <tt>:precision</tt> (e.g., 112.32 has a precision of 2).
# level of precision is 3. # You can customize the format in the +options+ hash.
#
# ==== Options
# * <tt>:precision</tt> - Sets the level of precision (defaults to 3).
# * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to "").
# #
# ==== Examples # ==== Examples
# number_with_precision(111.2345) # => 111.235 # number_with_precision(111.2345) # => 111.235
# number_with_precision(111.2345, 2) # => 111.23 # number_with_precision(111.2345, :precision => 2) # => 111.23
# number_with_precision(13, 5) # => 13.00000 # number_with_precision(13, :precision => 5) # => 13.00000
# number_with_precision(389.32314, 0) # => 389 # number_with_precision(389.32314, :precision => 0) # => 389
def number_with_precision(number, precision=3) # number_with_precision(1111.2345, :precision => 2, :separator => ',', :delimiter => '.')
"%01.#{precision}f" % ((Float(number) * (10 ** precision)).round.to_f / 10 ** precision) # # => 1.111,23
#
# You can still use <tt>number_with_precision</tt> with the old API that accepts the
# +precision+ as its optional second parameter:
# number_with_precision(number_with_precision(111.2345, 2) # => 111.23
def number_with_precision(number, *args)
options = args.extract_options!
options.symbolize_keys!
defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {}
precision_defaults = I18n.translate(:'number.precision.format', :locale => options[:locale],
:raise => true) rescue {}
defaults = defaults.merge(precision_defaults)
unless args.empty?
ActiveSupport::Deprecation.warn('number_with_precision takes an option hash ' +
'instead of a separate precision argument.', caller)
precision = args[0] || defaults[:precision]
end
precision ||= (options[:precision] || defaults[:precision])
separator ||= (options[:separator] || defaults[:separator])
delimiter ||= (options[:delimiter] || defaults[:delimiter])
begin
rounded_number = (Float(number) * (10 ** precision)).round.to_f / 10 ** precision
number_with_delimiter("%01.#{precision}f" % rounded_number,
:separator => separator,
:delimiter => delimiter)
rescue rescue
number number
end end
end
STORAGE_UNITS = %w( Bytes KB MB GB TB ).freeze
# Formats the bytes in +size+ into a more understandable representation # Formats the bytes in +size+ into a more understandable representation
# (e.g., giving it 1500 yields 1.5 KB). This method is useful for # (e.g., giving it 1500 yields 1.5 KB). This method is useful for
# reporting file sizes to users. This method returns nil if # reporting file sizes to users. This method returns nil if
# +size+ cannot be converted into a number. You can change the default # +size+ cannot be converted into a number. You can customize the
# precision of 1 using the precision parameter +precision+. # format in the +options+ hash.
#
# ==== Options
# * <tt>:precision</tt> - Sets the level of precision (defaults to 1).
# * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to "").
# #
# ==== Examples # ==== Examples
# number_to_human_size(123) # => 123 Bytes # number_to_human_size(123) # => 123 Bytes
@ -166,20 +240,51 @@ module ActionView
# number_to_human_size(1234567) # => 1.2 MB # number_to_human_size(1234567) # => 1.2 MB
# number_to_human_size(1234567890) # => 1.1 GB # number_to_human_size(1234567890) # => 1.1 GB
# number_to_human_size(1234567890123) # => 1.1 TB # number_to_human_size(1234567890123) # => 1.1 TB
# number_to_human_size(1234567, :precision => 2) # => 1.18 MB
# number_to_human_size(483989, :precision => 0) # => 473 KB
# number_to_human_size(1234567, :precision => 2, :separator => ',') # => 1,18 MB
#
# You can still use <tt>number_to_human_size</tt> with the old API that accepts the
# +precision+ as its optional second parameter:
# number_to_human_size(1234567, 2) # => 1.18 MB # number_to_human_size(1234567, 2) # => 1.18 MB
# number_to_human_size(483989, 0) # => 4 MB # number_to_human_size(483989, 0) # => 473 KB
def number_to_human_size(size, precision=1) def number_to_human_size(number, *args)
size = Kernel.Float(size) return number.nil? ? nil : pluralize(number.to_i, "Byte") if number.to_i < 1024
case
when size.to_i == 1; "1 Byte" options = args.extract_options!
when size < 1.kilobyte; "%d Bytes" % size options.symbolize_keys!
when size < 1.megabyte; "%.#{precision}f KB" % (size / 1.0.kilobyte)
when size < 1.gigabyte; "%.#{precision}f MB" % (size / 1.0.megabyte) defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {}
when size < 1.terabyte; "%.#{precision}f GB" % (size / 1.0.gigabyte) human = I18n.translate(:'number.human.format', :locale => options[:locale], :raise => true) rescue {}
else "%.#{precision}f TB" % (size / 1.0.terabyte) defaults = defaults.merge(human)
end.sub(/([0-9]\.\d*?)0+ /, '\1 ' ).sub(/\. /,' ')
unless args.empty?
ActiveSupport::Deprecation.warn('number_to_human_size takes an option hash ' +
'instead of a separate precision argument.', caller)
precision = args[0] || defaults[:precision]
end
precision ||= (options[:precision] || defaults[:precision])
separator ||= (options[:separator] || defaults[:separator])
delimiter ||= (options[:delimiter] || defaults[:delimiter])
max_exp = STORAGE_UNITS.size - 1
number = Float(number)
exponent = (Math.log(number) / Math.log(1024)).to_i # Convert to base 1024
exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
number /= 1024 ** exponent
unit = STORAGE_UNITS[exponent]
begin
escaped_separator = Regexp.escape(separator)
number_with_precision(number,
:precision => precision,
:separator => separator,
:delimiter => delimiter
).sub(/(\d)(#{escaped_separator}[1-9]*)?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '') + " #{unit}"
rescue rescue
nil number
end
end end
end end
end end

View file

@ -61,7 +61,7 @@ module ActionView
# #
# == Designing your Rails actions for Ajax # == Designing your Rails actions for Ajax
# When building your action handlers (that is, the Rails actions that receive your background requests), it's # When building your action handlers (that is, the Rails actions that receive your background requests), it's
# important to remember a few things. First, whatever your action would normall return to the browser, it will # important to remember a few things. First, whatever your action would normally return to the browser, it will
# return to the Ajax call. As such, you typically don't want to render with a layout. This call will cause # return to the Ajax call. As such, you typically don't want to render with a layout. This call will cause
# the layout to be transmitted back to your page, and, if you have a full HTML/CSS, will likely mess a lot of things up. # the layout to be transmitted back to your page, and, if you have a full HTML/CSS, will likely mess a lot of things up.
# You can turn the layout off on particular actions by doing the following: # You can turn the layout off on particular actions by doing the following:
@ -255,6 +255,14 @@ module ActionView
link_to_function(name, remote_function(options), html_options || options.delete(:html)) link_to_function(name, remote_function(options), html_options || options.delete(:html))
end end
# Creates a button with an onclick event which calls a remote action
# via XMLHttpRequest
# The options for specifying the target with :url
# and defining callbacks is the same as link_to_remote.
def button_to_remote(name, options = {}, html_options = {})
button_to_function(name, remote_function(options), html_options)
end
# Periodically calls the specified url (<tt>options[:url]</tt>) every # Periodically calls the specified url (<tt>options[:url]</tt>) every
# <tt>options[:frequency]</tt> seconds (default is 10). Usually used to # <tt>options[:frequency]</tt> seconds (default is 10). Usually used to
# update a specified div (<tt>options[:update]</tt>) with the results # update a specified div (<tt>options[:update]</tt>) with the results
@ -382,9 +390,9 @@ module ActionView
args.unshift object args.unshift object
end end
concat(form_remote_tag(options), proc.binding) concat(form_remote_tag(options))
fields_for(object_name, *(args << options), &proc) fields_for(object_name, *(args << options), &proc)
concat('</form>', proc.binding) concat('</form>')
end end
alias_method :form_remote_for, :remote_form_for alias_method :form_remote_for, :remote_form_for
@ -412,13 +420,10 @@ module ActionView
def submit_to_remote(name, value, options = {}) def submit_to_remote(name, value, options = {})
options[:with] ||= 'Form.serialize(this.form)' options[:with] ||= 'Form.serialize(this.form)'
options[:html] ||= {} html_options = options.delete(:html) || {}
options[:html][:type] = 'button' html_options[:name] = name
options[:html][:onclick] = "#{remote_function(options)}; return false;"
options[:html][:name] = name
options[:html][:value] = value
tag("input", options[:html], false) button_to_remote(value, options, html_options)
end end
# Returns '<tt>eval(request.responseText)</tt>' which is the JavaScript function # Returns '<tt>eval(request.responseText)</tt>' which is the JavaScript function
@ -578,14 +583,14 @@ module ActionView
def initialize(context, &block) #:nodoc: def initialize(context, &block) #:nodoc:
@context, @lines = context, [] @context, @lines = context, []
include_helpers_from_context include_helpers_from_context
@context.with_output_buffer(@lines) do
@context.instance_exec(self, &block) @context.instance_exec(self, &block)
end end
end
private private
def include_helpers_from_context def include_helpers_from_context
@context.extended_by.each do |mod| extend @context.helpers if @context.respond_to?(:helpers)
extend mod unless mod.name =~ /^ActionView::Helpers/
end
extend GeneratorMethods extend GeneratorMethods
end end
@ -603,7 +608,7 @@ module ActionView
# Example: # Example:
# #
# # Generates: # # Generates:
# # new Element.insert("list", { bottom: <li>Some item</li>" }); # # new Element.insert("list", { bottom: "<li>Some item</li>" });
# # new Effect.Highlight("list"); # # new Effect.Highlight("list");
# # ["status-indicator", "cancel-link"].each(Element.hide); # # ["status-indicator", "cancel-link"].each(Element.hide);
# update_page do |page| # update_page do |page|
@ -868,6 +873,16 @@ module ActionView
record "window.location.href = #{url.inspect}" record "window.location.href = #{url.inspect}"
end end
# Reloads the browser's current +location+ using JavaScript
#
# Examples:
#
# # Generates: window.location.reload();
# page.reload
def reload
record 'window.location.reload()'
end
# Calls the JavaScript +function+, optionally with the given +arguments+. # Calls the JavaScript +function+, optionally with the given +arguments+.
# #
# If a block is given, the block will be passed to a new JavaScriptGenerator; # If a block is given, the block will be passed to a new JavaScriptGenerator;

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