2009-04-23 18:58:38 -04:00
|
|
|
require "pathname"
|
2009-09-02 18:00:22 -04:00
|
|
|
require "active_support/core_ext/class"
|
2013-12-02 16:36:58 -05:00
|
|
|
require "active_support/core_ext/module/attribute_accessors"
|
2014-10-28 19:17:33 -04:00
|
|
|
require 'active_support/core_ext/string/filters'
|
2009-12-02 23:01:01 -05:00
|
|
|
require "action_view/template"
|
2012-05-20 01:03:34 -04:00
|
|
|
require "thread"
|
2012-12-13 08:47:33 -05:00
|
|
|
require "thread_safe"
|
2009-04-23 18:58:38 -04:00
|
|
|
|
2009-04-14 20:22:51 -04:00
|
|
|
module ActionView
|
2010-06-20 16:26:31 -04:00
|
|
|
# = Action View Resolver
|
2009-06-17 18:32:55 -04:00
|
|
|
class Resolver
|
2011-03-19 18:21:11 -04:00
|
|
|
# Keeps all information about view path and builds virtual path.
|
2012-06-21 15:13:13 -04:00
|
|
|
class Path
|
2011-03-19 00:13:59 -04:00
|
|
|
attr_reader :name, :prefix, :partial, :virtual
|
|
|
|
alias_method :partial?, :partial
|
|
|
|
|
2011-05-09 04:42:54 -04:00
|
|
|
def self.build(name, prefix, partial)
|
|
|
|
virtual = ""
|
|
|
|
virtual << "#{prefix}/" unless prefix.empty?
|
|
|
|
virtual << (partial ? "_#{name}" : name)
|
|
|
|
new name, prefix, partial, virtual
|
2011-03-19 00:13:59 -04:00
|
|
|
end
|
|
|
|
|
2011-05-09 04:42:54 -04:00
|
|
|
def initialize(name, prefix, partial, virtual)
|
2012-06-21 15:13:13 -04:00
|
|
|
@name = name
|
|
|
|
@prefix = prefix
|
|
|
|
@partial = partial
|
|
|
|
@virtual = virtual
|
2011-03-19 00:13:59 -04:00
|
|
|
end
|
2012-06-21 15:13:13 -04:00
|
|
|
|
|
|
|
def to_str
|
|
|
|
@virtual
|
|
|
|
end
|
|
|
|
alias :to_s :to_str
|
2011-03-19 00:13:59 -04:00
|
|
|
end
|
|
|
|
|
2012-05-20 01:03:34 -04:00
|
|
|
# Threadsafe template cache
|
|
|
|
class Cache #:nodoc:
|
2012-12-13 08:47:33 -05:00
|
|
|
class SmallCache < ThreadSafe::Cache
|
|
|
|
def initialize(options = {})
|
|
|
|
super(options.merge(:initial_capacity => 2))
|
|
|
|
end
|
2012-05-21 09:41:04 -04:00
|
|
|
end
|
|
|
|
|
2012-12-13 08:47:33 -05:00
|
|
|
# preallocate all the default blocks for performance/memory consumption reasons
|
|
|
|
PARTIAL_BLOCK = lambda {|cache, partial| cache[partial] = SmallCache.new}
|
|
|
|
PREFIX_BLOCK = lambda {|cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK)}
|
|
|
|
NAME_BLOCK = lambda {|cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK)}
|
|
|
|
KEY_BLOCK = lambda {|cache, key| cache[key] = SmallCache.new(&NAME_BLOCK)}
|
|
|
|
|
2013-04-12 10:35:02 -04:00
|
|
|
# usually a majority of template look ups return nothing, use this canonical preallocated array to save memory
|
2012-12-13 08:47:33 -05:00
|
|
|
NO_TEMPLATES = [].freeze
|
|
|
|
|
2012-05-20 01:03:34 -04:00
|
|
|
def initialize
|
2012-12-13 08:47:33 -05:00
|
|
|
@data = SmallCache.new(&KEY_BLOCK)
|
2012-05-20 01:03:34 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
# Cache the templates returned by the block
|
|
|
|
def cache(key, name, prefix, partial, locals)
|
2012-12-13 08:47:33 -05:00
|
|
|
if Resolver.caching?
|
|
|
|
@data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
|
|
|
|
else
|
|
|
|
fresh_templates = yield
|
|
|
|
cached_templates = @data[key][name][prefix][partial][locals]
|
2012-05-21 09:41:04 -04:00
|
|
|
|
2012-12-13 08:47:33 -05:00
|
|
|
if templates_have_changed?(cached_templates, fresh_templates)
|
|
|
|
@data[key][name][prefix][partial][locals] = canonical_no_templates(fresh_templates)
|
2012-05-20 01:03:34 -04:00
|
|
|
else
|
2012-12-13 08:47:33 -05:00
|
|
|
cached_templates || NO_TEMPLATES
|
2012-05-20 01:03:34 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def clear
|
2012-12-13 08:47:33 -05:00
|
|
|
@data.clear
|
2012-05-20 01:03:34 -04:00
|
|
|
end
|
2012-05-21 21:31:40 -04:00
|
|
|
|
|
|
|
private
|
|
|
|
|
2012-12-13 08:47:33 -05:00
|
|
|
def canonical_no_templates(templates)
|
|
|
|
templates.empty? ? NO_TEMPLATES : templates
|
|
|
|
end
|
|
|
|
|
2012-05-21 21:31:40 -04:00
|
|
|
def templates_have_changed?(cached_templates, fresh_templates)
|
|
|
|
# if either the old or new template list is empty, we don't need to (and can't)
|
|
|
|
# compare modification times, and instead just check whether the lists are different
|
|
|
|
if cached_templates.blank? || fresh_templates.blank?
|
|
|
|
return fresh_templates.blank? != cached_templates.blank?
|
|
|
|
end
|
|
|
|
|
|
|
|
cached_templates_max_updated_at = cached_templates.map(&:updated_at).max
|
|
|
|
|
|
|
|
# if a template has changed, it will be now be newer than all the cached templates
|
|
|
|
fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at }
|
|
|
|
end
|
2012-05-20 01:03:34 -04:00
|
|
|
end
|
|
|
|
|
2011-12-08 12:54:56 -05:00
|
|
|
cattr_accessor :caching
|
2010-12-16 15:37:48 -05:00
|
|
|
self.caching = true
|
|
|
|
|
|
|
|
class << self
|
|
|
|
alias :caching? :caching
|
|
|
|
end
|
|
|
|
|
2010-03-07 06:49:27 -05:00
|
|
|
def initialize
|
2012-05-20 01:03:34 -04:00
|
|
|
@cache = Cache.new
|
2009-06-17 18:32:55 -04:00
|
|
|
end
|
|
|
|
|
2010-03-08 17:13:24 -05:00
|
|
|
def clear_cache
|
2012-05-20 01:03:34 -04:00
|
|
|
@cache.clear
|
2010-03-08 17:13:24 -05:00
|
|
|
end
|
|
|
|
|
2013-03-06 08:20:09 -05:00
|
|
|
# Normalizes the arguments and passes it on to find_templates.
|
2010-12-09 08:06:44 -05:00
|
|
|
def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
|
2010-10-10 03:24:17 -04:00
|
|
|
cached(key, [name, prefix, partial], details, locals) do
|
2010-03-08 17:13:24 -05:00
|
|
|
find_templates(name, prefix, partial, details)
|
2009-09-02 18:00:22 -04:00
|
|
|
end
|
2009-04-22 20:16:28 -04:00
|
|
|
end
|
2009-09-02 18:00:22 -04:00
|
|
|
|
2009-06-17 18:32:55 -04:00
|
|
|
private
|
|
|
|
|
2013-01-06 04:43:30 -05:00
|
|
|
delegate :caching?, to: :class
|
2010-03-07 06:49:27 -05:00
|
|
|
|
2009-06-17 18:32:55 -04:00
|
|
|
# This is what child classes implement. No defaults are needed
|
|
|
|
# because Resolver guarantees that the arguments are present and
|
|
|
|
# normalized.
|
2010-03-08 17:13:24 -05:00
|
|
|
def find_templates(name, prefix, partial, details)
|
2011-02-09 08:32:40 -05:00
|
|
|
raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details) method"
|
2009-06-17 18:32:55 -04:00
|
|
|
end
|
|
|
|
|
2010-10-07 07:26:58 -04:00
|
|
|
# Helpers that builds a path. Useful for building virtual paths.
|
2010-10-10 03:24:17 -04:00
|
|
|
def build_path(name, prefix, partial)
|
2011-05-09 04:42:54 -04:00
|
|
|
Path.build(name, prefix, partial)
|
2010-10-07 07:26:58 -04:00
|
|
|
end
|
|
|
|
|
2010-12-27 02:44:51 -05:00
|
|
|
# Handles templates caching. If a key is given and caching is on
|
2010-10-10 03:24:17 -04:00
|
|
|
# always check the cache before hitting the resolver. Otherwise,
|
2012-05-20 01:03:34 -04:00
|
|
|
# it always hits the resolver but if the key is present, check if the
|
|
|
|
# resolver is fresher before returning it.
|
2010-10-10 03:24:17 -04:00
|
|
|
def cached(key, path_info, details, locals) #:nodoc:
|
|
|
|
name, prefix, partial = path_info
|
2011-08-09 13:13:02 -04:00
|
|
|
locals = locals.map { |x| x.to_s }.sort!
|
2010-10-10 03:24:17 -04:00
|
|
|
|
2012-05-20 01:03:34 -04:00
|
|
|
if key
|
|
|
|
@cache.cache(key, name, prefix, partial, locals) do
|
|
|
|
decorate(yield, path_info, details, locals)
|
2012-05-21 15:01:02 -04:00
|
|
|
end
|
2012-05-20 01:03:34 -04:00
|
|
|
else
|
|
|
|
decorate(yield, path_info, details, locals)
|
2010-10-10 03:24:17 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Ensures all the resolver information is set in the template.
|
|
|
|
def decorate(templates, path_info, details, locals) #:nodoc:
|
|
|
|
cached = nil
|
|
|
|
templates.each do |t|
|
|
|
|
t.locals = locals
|
2014-03-12 10:42:21 -04:00
|
|
|
t.formats = details[:formats] || [:html] if t.formats.empty?
|
|
|
|
t.variants = details[:variants] || [] if t.variants.empty?
|
2010-10-10 03:24:17 -04:00
|
|
|
t.virtual_path ||= (cached ||= build_path(*path_info))
|
2010-10-07 09:50:20 -04:00
|
|
|
end
|
|
|
|
end
|
2009-06-17 18:32:55 -04:00
|
|
|
end
|
2009-04-14 20:22:51 -04:00
|
|
|
|
2011-05-09 05:17:24 -04:00
|
|
|
# An abstract class that implements a Resolver with path semantics.
|
|
|
|
class PathResolver < Resolver #:nodoc:
|
2013-12-03 05:17:01 -05:00
|
|
|
EXTENSIONS = { :locale => ".", :formats => ".", :variants => "+", :handlers => "." }
|
|
|
|
DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
|
2011-03-19 00:13:59 -04:00
|
|
|
|
|
|
|
def initialize(pattern=nil)
|
|
|
|
@pattern = pattern || DEFAULT_PATTERN
|
|
|
|
super()
|
|
|
|
end
|
2009-06-17 18:32:55 -04:00
|
|
|
|
2010-10-07 07:26:58 -04:00
|
|
|
private
|
2009-06-17 18:32:55 -04:00
|
|
|
|
2010-03-08 17:13:24 -05:00
|
|
|
def find_templates(name, prefix, partial, details)
|
2011-05-09 04:42:54 -04:00
|
|
|
path = Path.build(name, prefix, partial)
|
2011-05-09 05:17:24 -04:00
|
|
|
query(path, details, details[:formats])
|
2009-06-17 18:32:55 -04:00
|
|
|
end
|
2009-09-02 18:00:22 -04:00
|
|
|
|
2011-05-09 05:17:24 -04:00
|
|
|
def query(path, details, formats)
|
|
|
|
query = build_query(path, details)
|
2011-08-09 12:32:16 -04:00
|
|
|
|
2014-05-10 14:52:13 -04:00
|
|
|
template_paths = find_template_paths query
|
2010-05-17 11:41:54 -04:00
|
|
|
|
2011-08-09 12:32:16 -04:00
|
|
|
template_paths.map { |template|
|
2014-03-12 10:42:21 -04:00
|
|
|
handler, format, variant = extract_handler_and_format_and_variant(template, formats)
|
|
|
|
contents = File.binread(template)
|
2010-05-17 11:41:54 -04:00
|
|
|
|
2011-08-09 12:32:16 -04:00
|
|
|
Template.new(contents, File.expand_path(template), handler,
|
|
|
|
:virtual_path => path.virtual,
|
|
|
|
:format => format,
|
2014-03-12 10:42:21 -04:00
|
|
|
:variant => variant,
|
|
|
|
:updated_at => mtime(template)
|
|
|
|
)
|
2011-08-09 12:32:16 -04:00
|
|
|
}
|
2009-04-14 20:22:51 -04:00
|
|
|
end
|
2009-04-23 18:58:38 -04:00
|
|
|
|
2014-05-18 15:00:03 -04:00
|
|
|
if RUBY_VERSION >= '2.2.0'
|
2014-05-10 14:52:13 -04:00
|
|
|
def find_template_paths(query)
|
|
|
|
Dir[query].reject { |filename|
|
|
|
|
File.directory?(filename) ||
|
|
|
|
# deals with case-insensitive file systems.
|
|
|
|
!File.fnmatch(query, filename, File::FNM_EXTGLOB)
|
|
|
|
}
|
|
|
|
end
|
|
|
|
else
|
|
|
|
def find_template_paths(query)
|
|
|
|
# deals with case-insensitive file systems.
|
|
|
|
sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] }
|
|
|
|
|
|
|
|
Dir[query].reject { |filename|
|
|
|
|
File.directory?(filename) ||
|
|
|
|
!sanitizer[File.dirname(filename)].include?(filename)
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-05-09 04:42:54 -04:00
|
|
|
# Helper for building query glob string based on resolver's pattern.
|
2011-05-09 05:17:24 -04:00
|
|
|
def build_query(path, details)
|
2011-03-19 00:13:59 -04:00
|
|
|
query = @pattern.dup
|
2011-08-16 18:16:45 -04:00
|
|
|
|
|
|
|
prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
|
|
|
|
query.gsub!(/\:prefix(\/)?/, prefix)
|
|
|
|
|
|
|
|
partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
|
|
|
|
query.gsub!(/\:action/, partial)
|
2011-03-19 00:13:59 -04:00
|
|
|
|
2011-05-09 05:17:24 -04:00
|
|
|
details.each do |ext, variants|
|
2011-03-19 00:13:59 -04:00
|
|
|
query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}")
|
2011-05-09 05:17:24 -04:00
|
|
|
end
|
2011-03-19 00:13:59 -04:00
|
|
|
|
|
|
|
File.expand_path(query, @path)
|
|
|
|
end
|
|
|
|
|
2011-08-16 18:16:45 -04:00
|
|
|
def escape_entry(entry)
|
2011-08-18 05:59:02 -04:00
|
|
|
entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
|
2011-08-16 18:16:45 -04:00
|
|
|
end
|
|
|
|
|
2010-10-10 03:24:17 -04:00
|
|
|
# Returns the file mtime from the filesystem.
|
|
|
|
def mtime(p)
|
2011-07-25 11:01:30 -04:00
|
|
|
File.mtime(p)
|
2010-10-10 03:24:17 -04:00
|
|
|
end
|
|
|
|
|
2014-04-25 21:10:05 -04:00
|
|
|
# Extract handler, formats and variant from path. If a format cannot be found neither
|
2010-03-19 12:20:15 -04:00
|
|
|
# from the path, or the handler, we should return the array of formats given
|
|
|
|
# to the resolver.
|
2014-03-12 10:42:21 -04:00
|
|
|
def extract_handler_and_format_and_variant(path, default_formats)
|
2010-03-09 07:12:11 -05:00
|
|
|
pieces = File.basename(path).split(".")
|
|
|
|
pieces.shift
|
2012-10-29 11:22:59 -04:00
|
|
|
|
2012-05-13 20:22:29 -04:00
|
|
|
extension = pieces.pop
|
2012-10-29 11:22:59 -04:00
|
|
|
unless extension
|
2014-10-28 19:17:33 -04:00
|
|
|
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
|
|
|
The file #{path} did not specify a template handler. The default is
|
|
|
|
currently ERB, but will change to RAW in the future.
|
|
|
|
MSG
|
2012-10-29 11:22:59 -04:00
|
|
|
end
|
|
|
|
|
2012-05-13 20:22:29 -04:00
|
|
|
handler = Template.handler_for_extension(extension)
|
2014-03-12 10:42:21 -04:00
|
|
|
format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
|
2013-12-03 05:17:01 -05:00
|
|
|
format &&= Template::Types[format]
|
|
|
|
|
2014-03-12 10:42:21 -04:00
|
|
|
[handler, format, variant]
|
2009-04-23 18:58:38 -04:00
|
|
|
end
|
2009-04-14 20:22:51 -04:00
|
|
|
end
|
2009-06-17 18:32:55 -04:00
|
|
|
|
2012-03-13 20:50:39 -04:00
|
|
|
# A resolver that loads files from the filesystem. It allows setting your own
|
2011-03-19 18:21:11 -04:00
|
|
|
# resolving pattern. Such pattern can be a glob string supported by some variables.
|
|
|
|
#
|
|
|
|
# ==== Examples
|
|
|
|
#
|
|
|
|
# Default pattern, loads views the same way as previous versions of rails, eg. when you're
|
2011-03-29 16:23:12 -04:00
|
|
|
# looking for `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml},}`
|
2011-03-19 18:21:11 -04:00
|
|
|
#
|
2014-09-26 13:26:16 -04:00
|
|
|
# FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
|
2011-03-19 18:21:11 -04:00
|
|
|
#
|
2013-03-26 10:42:45 -04:00
|
|
|
# This one allows you to keep files with different formats in separate subdirectories,
|
2011-03-19 18:21:11 -04:00
|
|
|
# eg. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`,
|
|
|
|
# `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, etc.
|
|
|
|
#
|
2014-09-26 13:26:16 -04:00
|
|
|
# FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
|
2011-03-19 18:21:11 -04:00
|
|
|
#
|
2012-03-13 20:50:39 -04:00
|
|
|
# If you don't specify a pattern then the default will be used.
|
2011-03-19 18:21:11 -04:00
|
|
|
#
|
2011-03-19 19:06:50 -04:00
|
|
|
# In order to use any of the customized resolvers above in a Rails application, you just need
|
|
|
|
# to configure ActionController::Base.view_paths in an initializer, for example:
|
|
|
|
#
|
|
|
|
# ActionController::Base.view_paths = FileSystemResolver.new(
|
|
|
|
# Rails.root.join("app/views"),
|
2014-09-26 13:26:16 -04:00
|
|
|
# ":prefix{/:locale}/:action{.:formats,}{+:variants,}{.:handlers,}"
|
2011-03-19 19:06:50 -04:00
|
|
|
# )
|
|
|
|
#
|
2011-03-19 18:21:11 -04:00
|
|
|
# ==== Pattern format and variables
|
|
|
|
#
|
2012-03-13 20:50:39 -04:00
|
|
|
# Pattern has to be a valid glob string, and it allows you to use the
|
2011-03-19 18:21:11 -04:00
|
|
|
# following variables:
|
|
|
|
#
|
2012-03-13 20:50:39 -04:00
|
|
|
# * <tt>:prefix</tt> - usually the controller path
|
2011-03-19 18:21:11 -04:00
|
|
|
# * <tt>:action</tt> - name of the action
|
|
|
|
# * <tt>:locale</tt> - possible locale versions
|
2011-03-19 19:06:50 -04:00
|
|
|
# * <tt>:formats</tt> - possible request formats (for example html, json, xml...)
|
2014-09-26 13:26:16 -04:00
|
|
|
# * <tt>:variants</tt> - possible request variants (for example phone, tablet...)
|
2011-03-19 19:06:50 -04:00
|
|
|
# * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...)
|
2011-03-19 18:21:11 -04:00
|
|
|
#
|
2009-09-02 18:00:22 -04:00
|
|
|
class FileSystemResolver < PathResolver
|
2011-03-19 00:13:59 -04:00
|
|
|
def initialize(path, pattern=nil)
|
2009-09-02 18:00:22 -04:00
|
|
|
raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
|
2011-03-19 00:13:59 -04:00
|
|
|
super(pattern)
|
2010-06-23 19:04:41 -04:00
|
|
|
@path = File.expand_path(path)
|
2009-09-02 18:00:22 -04:00
|
|
|
end
|
2009-06-17 18:32:55 -04:00
|
|
|
|
2010-10-07 07:26:58 -04:00
|
|
|
def to_s
|
|
|
|
@path.to_s
|
|
|
|
end
|
|
|
|
alias :to_path :to_s
|
|
|
|
|
2010-03-08 10:32:40 -05:00
|
|
|
def eql?(resolver)
|
|
|
|
self.class.equal?(resolver.class) && to_path == resolver.to_path
|
2009-09-02 18:00:22 -04:00
|
|
|
end
|
2010-03-08 10:32:40 -05:00
|
|
|
alias :== :eql?
|
2009-06-17 18:32:55 -04:00
|
|
|
end
|
2010-10-10 17:11:50 -04:00
|
|
|
|
2011-05-09 05:17:24 -04:00
|
|
|
# An Optimized resolver for Rails' most common case.
|
|
|
|
class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
|
|
|
|
def build_query(path, details)
|
2011-08-16 18:16:45 -04:00
|
|
|
query = escape_entry(File.join(@path, path))
|
2011-05-09 05:17:24 -04:00
|
|
|
|
2013-12-03 05:17:01 -05:00
|
|
|
exts = EXTENSIONS.map do |ext, prefix|
|
|
|
|
"{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}"
|
|
|
|
end.join
|
|
|
|
|
|
|
|
query + exts
|
2011-05-09 05:17:24 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2010-10-10 17:11:50 -04:00
|
|
|
# The same as FileSystemResolver but does not allow templates to store
|
|
|
|
# a virtual path since it is invalid for such resolvers.
|
2011-05-09 05:17:24 -04:00
|
|
|
class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
|
2010-10-10 17:11:50 -04:00
|
|
|
def self.instances
|
|
|
|
[new(""), new("/")]
|
|
|
|
end
|
|
|
|
|
|
|
|
def decorate(*)
|
|
|
|
super.each { |t| t.virtual_path = nil }
|
|
|
|
end
|
|
|
|
end
|
2009-12-28 19:28:26 -05:00
|
|
|
end
|