Adds a branch for unlimited project hierarchy.

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/work@2148 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang
2008-12-19 20:51:35 +00:00
parent 7ae224f5c2
commit 04fea923a4
1272 changed files with 119640 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
require 'active_support'
require File.join(File.dirname(__FILE__), 'engines/plugin')
require File.join(File.dirname(__FILE__), 'engines/plugin/list')
require File.join(File.dirname(__FILE__), 'engines/plugin/loader')
require File.join(File.dirname(__FILE__), 'engines/plugin/locator')
require File.join(File.dirname(__FILE__), 'engines/assets')
require File.join(File.dirname(__FILE__), 'engines/rails_extensions/rails')
# == Parameters
#
# The Engines module has a number of public configuration parameters:
#
# [+public_directory+] The directory into which plugin assets should be
# mirrored. Defaults to <tt>RAILS_ROOT/public/plugin_assets</tt>.
# [+schema_info_table+] The table to use when storing plugin migration
# version information. Defaults to +plugin_schema_info+.
#
# Additionally, there are a few flags which control the behaviour of
# some of the features the engines plugin adds to Rails:
#
# [+disable_application_view_loading+] A boolean flag determining whether
# or not views should be loaded from
# the main <tt>app/views</tt> directory.
# Defaults to false; probably only
# useful when testing your plugin.
# [+disable_application_code_loading+] A boolean flag determining whether
# or not to load controllers/helpers
# from the main +app+ directory,
# if corresponding code exists within
# a plugin. Defaults to false; again,
# probably only useful when testing
# your plugin.
# [+disable_code_mixing+] A boolean flag indicating whether all plugin
# copies of a particular controller/helper should
# be loaded and allowed to override each other,
# or if the first matching file should be loaded
# instead. Defaults to false.
#
module Engines
# The set of all loaded plugins
mattr_accessor :plugins
self.plugins = Engines::Plugin::List.new
# List of extensions to load, can be changed in init.rb before calling Engines.init
mattr_accessor :rails_extensions
self.rails_extensions = %w(action_mailer asset_helpers routing migrations dependencies)
# The name of the public directory to mirror public engine assets into.
# Defaults to <tt>RAILS_ROOT/public/plugin_assets</tt>.
mattr_accessor :public_directory
self.public_directory = File.join(RAILS_ROOT, 'public', 'plugin_assets')
# The table in which to store plugin schema information. Defaults to
# "plugin_schema_info".
mattr_accessor :schema_info_table
self.schema_info_table = "plugin_schema_info"
#--
# These attributes control the behaviour of the engines extensions
#++
# Set this to true if views should *only* be loaded from plugins
mattr_accessor :disable_application_view_loading
self.disable_application_view_loading = false
# Set this to true if controller/helper code shouldn't be loaded
# from the application
mattr_accessor :disable_application_code_loading
self.disable_application_code_loading = false
# Set this ti true if code should not be mixed (i.e. it will be loaded
# from the first valid path on $LOAD_PATH)
mattr_accessor :disable_code_mixing
self.disable_code_mixing = false
# This is used to determine which files are candidates for the "code
# mixing" feature that the engines plugin provides, where classes from
# plugins can be loaded, and then code from the application loaded
# on top of that code to override certain methods.
mattr_accessor :code_mixing_file_types
self.code_mixing_file_types = %w(controller helper)
class << self
def init
load_extensions
Engines::Assets.initialize_base_public_directory
end
def logger
RAILS_DEFAULT_LOGGER
end
def load_extensions
rails_extensions.each { |name| require "engines/rails_extensions/#{name}" }
# load the testing extensions, if we are in the test environment.
require "engines/testing" if RAILS_ENV == "test"
end
def select_existing_paths(paths)
paths.select { |path| File.directory?(path) }
end
# The engines plugin will, by default, mix code from controllers and helpers,
# allowing application code to override specific methods in the corresponding
# controller or helper classes and modules. However, if other file types should
# also be mixed like this, they can be added by calling this method. For example,
# if you want to include "things" within your plugin and override them from
# your applications, you should use the following layout:
#
# app/
# +-- things/
# | +-- one_thing.rb
# | +-- another_thing.rb
# ...
# vendor/
# +-- plugins/
# +-- my_plugin/
# +-- app/
# +-- things/
# +-- one_thing.rb
# +-- another_thing.rb
#
# The important point here is that your "things" are named <whatever>_thing.rb,
# and that they are placed within plugin/app/things (the pluralized form of 'thing').
#
# It's important to note that you'll also want to ensure that the "things" are
# on your load path in your plugin's init.rb:
#
# Rails.plugins[:my_plugin].code_paths << "app/things"
#
def mix_code_from(*types)
self.code_mixing_file_types += types.map { |x| x.to_s.singularize }
end
# A general purpose method to mirror a directory (+source+) into a destination
# directory, including all files and subdirectories. Files will not be mirrored
# if they are identical already (checked via FileUtils#identical?).
def mirror_files_from(source, destination)
return unless File.directory?(source)
# TODO: use Rake::FileList#pathmap?
source_files = Dir[source + "/**/*"]
source_dirs = source_files.select { |d| File.directory?(d) }
source_files -= source_dirs
source_dirs.each do |dir|
# strip down these paths so we have simple, relative paths we can
# add to the destination
target_dir = File.join(destination, dir.gsub(source, ''))
begin
FileUtils.mkdir_p(target_dir)
rescue Exception => e
raise "Could not create directory #{target_dir}: \n" + e
end
end
source_files.each do |file|
begin
target = File.join(destination, file.gsub(source, ''))
unless File.exist?(target) && FileUtils.identical?(file, target)
FileUtils.cp(file, target)
end
rescue Exception => e
raise "Could not copy #{file} to #{target}: \n" + e
end
end
end
end
end
@@ -0,0 +1,38 @@
module Engines
module Assets
class << self
@@readme = %{Files in this directory are automatically generated from your plugins.
They are copied from the 'assets' directories of each plugin into this directory
each time Rails starts (script/server, script/console... and so on).
Any edits you make will NOT persist across the next server restart; instead you
should edit the files within the <plugin_name>/assets/ directory itself.}
# Ensure that the plugin asset subdirectory of RAILS_ROOT/public exists, and
# that we've added a little warning message to instruct developers not to mess with
# the files inside, since they're automatically generated.
def initialize_base_public_directory
dir = Engines.public_directory
unless File.exist?(dir)
Engines.logger.debug "Creating public engine files directory '#{dir}'"
FileUtils.mkdir_p(dir)
end
readme = File.join(dir, "README")
File.open(readme, 'w') { |f| f.puts @@readme } unless File.exist?(readme)
end
# Replicates the subdirectories under the plugins's +assets+ (or +public+)
# directory into the corresponding public directory. See also
# Plugin#public_directory for more.
def mirror_files_for(plugin)
return if plugin.public_directory.nil?
begin
Engines.logger.debug "Attempting to copy plugin assets from '#{plugin.public_directory}' to '#{Engines.public_directory}'"
Engines.mirror_files_from(plugin.public_directory, File.join(Engines.public_directory, plugin.name))
rescue Exception => e
Engines.logger.warn "WARNING: Couldn't create the public file structure for plugin '#{plugin.name}'; Error follows:"
Engines.logger.warn e
end
end
end
end
end
@@ -0,0 +1,126 @@
# An instance of Plugin is created for each plugin loaded by Rails, and
# stored in the <tt>Engines.plugins</tt> PluginList
# (see Engines::RailsExtensions::RailsInitializer for more details).
#
# Engines.plugins[:plugin_name]
#
# If this plugin contains paths in directories other than <tt>app/controllers</tt>,
# <tt>app/helpers</tt>, <tt>app/models</tt> and <tt>components</tt>, authors can
# declare this by adding extra paths to #code_paths:
#
# Rails.plugin[:my_plugin].code_paths << "app/sweepers" << "vendor/my_lib"
#
# Other properties of the Plugin instance can also be set.
module Engines
class Plugin < Rails::Plugin
# Plugins can add code paths to this attribute in init.rb if they
# need plugin directories to be added to the load path, i.e.
#
# plugin.code_paths << 'app/other_classes'
#
# Defaults to ["app/controllers", "app/helpers", "app/models", "components"]
attr_accessor :code_paths
# Plugins can add paths to this attribute in init.rb if they need
# controllers loaded from additional locations.
attr_accessor :controller_paths
# The directory in this plugin to mirror into the shared directory
# under +public+.
#
# Defaults to "assets" (see default_public_directory).
attr_accessor :public_directory
protected
# The default set of code paths which will be added to $LOAD_PATH
# and Dependencies.load_paths
def default_code_paths
# lib will actually be removed from the load paths when we call
# uniq! in #inject_into_load_paths, but it's important to keep it
# around (for the documentation tasks, for instance).
%w(app/controllers app/helpers app/models components lib)
end
# The default set of code paths which will be added to the routing system
def default_controller_paths
%w(app/controllers components)
end
# Attempts to detect the directory to use for public files.
# If +assets+ exists in the plugin, this will be used. If +assets+ is missing
# but +public+ is found, +public+ will be used.
def default_public_directory
Engines.select_existing_paths(%w(assets public).map { |p| File.join(directory, p) }).first
end
public
def initialize(directory)
super directory
@code_paths = default_code_paths
@controller_paths = default_controller_paths
@public_directory = default_public_directory
end
# Returns a list of paths this plugin wishes to make available in $LOAD_PATH
#
# Overwrites the correspondend method in the superclass
def load_paths
report_nonexistant_or_empty_plugin! unless valid?
select_existing_paths :code_paths
end
# Extends the superclass' load method to additionally mirror public assets
def load(initializer)
return if loaded?
super initializer
add_plugin_view_paths
Assets.mirror_files_for(self)
end
# for code_paths and controller_paths select those paths that actually
# exist in the plugin's directory
def select_existing_paths(name)
Engines.select_existing_paths(self.send(name).map { |p| File.join(directory, p) })
end
def add_plugin_view_paths
view_path = File.join(directory, 'app', 'views')
if File.exist?(view_path)
ActionController::Base.prepend_view_path(view_path) # push it just underneath the app
ActionView::TemplateFinder.process_view_paths(view_path)
end
end
# The path to this plugin's public files
def public_asset_directory
"#{File.basename(Engines.public_directory)}/#{name}"
end
# The path to this plugin's routes file
def routes_path
File.join(directory, "routes.rb")
end
# The directory containing this plugin's migrations (<tt>plugin/db/migrate</tt>)
def migration_directory
File.join(self.directory, 'db', 'migrate')
end
# Returns the version number of the latest migration for this plugin. Returns
# nil if this plugin has no migrations.
def latest_migration
migrations = Dir[migration_directory+"/*.rb"]
return nil if migrations.empty?
migrations.map { |p| File.basename(p) }.sort.last.match(/0*(\d+)\_/)[1].to_i
end
# Migrate this plugin to the given version. See Engines::Plugin::Migrator for more
# information.
def migrate(version = nil)
Engines::Plugin::Migrator.migrate_plugin(self, version)
end
end
end
@@ -0,0 +1,30 @@
# The PluginList class is an array, enhanced to allow access to loaded plugins
# by name, and iteration over loaded plugins in order of priority. This array is used
# by Engines::RailsExtensions::RailsInitializer to create the Engines.plugins array.
#
# Each loaded plugin has a corresponding Plugin instance within this array, and
# the order the plugins were loaded is reflected in the entries in this array.
#
# For more information, see the Rails module.
module Engines
class Plugin
class List < Array
# Finds plugins with the set with the given name (accepts Strings or Symbols), or
# index. So, Engines.plugins[0] returns the first-loaded Plugin, and Engines.plugins[:engines]
# returns the Plugin instance for the engines plugin itself.
def [](name_or_index)
if name_or_index.is_a?(Fixnum)
super
else
self.find { |plugin| plugin.name.to_s == name_or_index.to_s }
end
end
# Go through each plugin, highest priority first (last loaded first). Effectively,
# this is like <tt>Engines.plugins.reverse</tt>
def by_precedence
reverse
end
end
end
end
@@ -0,0 +1,18 @@
module Engines
class Plugin
class Loader < Rails::Plugin::Loader
protected
def register_plugin_as_loaded(plugin)
super plugin
Engines.plugins << plugin
register_to_routing(plugin)
end
# Registers the plugin's controller_paths for the routing system.
def register_to_routing(plugin)
initializer.configuration.controller_paths += plugin.select_existing_paths(:controller_paths)
initializer.configuration.controller_paths.uniq!
end
end
end
end
@@ -0,0 +1,11 @@
module Engines
class Plugin
class FileSystemLocator < Rails::Plugin::FileSystemLocator
def create_plugin(path)
plugin = Engines::Plugin.new(path)
plugin.valid? ? plugin : nil
end
end
end
end
@@ -0,0 +1,74 @@
# The Plugin::Migrator class contains the logic to run migrations from
# within plugin directories. The directory in which a plugin's migrations
# should be is determined by the Plugin#migration_directory method.
#
# To migrate a plugin, you can simple call the migrate method (Plugin#migrate)
# with the version number that plugin should be at. The plugin's migrations
# will then be used to migrate up (or down) to the given version.
#
# For more information, see Engines::RailsExtensions::Migrations
class Engines::Plugin::Migrator < ActiveRecord::Migrator
# We need to be able to set the 'current' engine being migrated.
cattr_accessor :current_plugin
# Runs the migrations from a plugin, up (or down) to the version given
def self.migrate_plugin(plugin, version)
self.current_plugin = plugin
# There seems to be a bug in Rails' own migrations, where migrating
# to the existing version causes all migrations to be run where that
# migration number doesn't exist (i.e. zero). We could fix this by
# removing the line if the version hits zero...?
return if current_version(plugin) == version
migrate(plugin.migration_directory, version)
end
# Returns the name of the table used to store schema information about
# installed plugins.
#
# See Engines.schema_info_table for more details.
def self.schema_info_table_name
proper_table_name Engines.schema_info_table
end
# Returns the current version of the given plugin
def self.current_version(plugin=current_plugin)
result = ActiveRecord::Base.connection.select_one(<<-ESQL
SELECT version FROM #{schema_info_table_name}
WHERE plugin_name = '#{plugin.name}'
ESQL
)
if result
result["version"].to_i
else
# There probably isn't an entry for this engine in the migration info table.
# We need to create that entry, and set the version to 0
ActiveRecord::Base.connection.execute(<<-ESQL
INSERT INTO #{schema_info_table_name} (version, plugin_name)
VALUES (0,'#{plugin.name}')
ESQL
)
0
end
end
def migrated(plugin=current_plugin)
current = ActiveRecord::Base.connection.select_value(<<-ESQL
SELECT version FROM #{self.class.schema_info_table_name}
WHERE plugin_name = '#{plugin.name}'
ESQL
).to_i
current ? (1..current).to_a : []
end
# Sets the version of the plugin in Engines::Plugin::Migrator.current_plugin to
# the given version.
def record_version_state_after_migrating(version)
ActiveRecord::Base.connection.update(<<-ESQL
UPDATE #{self.class.schema_info_table_name}
SET version = #{down? ? version.to_i - 1 : version.to_i}
WHERE plugin_name = '#{self.current_plugin.name}'
ESQL
)
end
end
@@ -0,0 +1,85 @@
# The way ActionMailer is coded in terms of finding templates is very restrictive, to the point
# where all templates for rendering must exist under the single base path. This is difficult to
# work around without re-coding significant parts of the action mailer code.
#
# ---
#
# The MailTemplates module overrides two (private) methods from ActionMailer to enable mail
# templates within plugins:
#
# [+template_path+] which now produces the contents of #template_paths
# [+initialize_template_class+] which now find the first matching template and creates
# an ActionVew::Base instance with the correct view_paths
#
# Ideally ActionMailer would use the same template-location logic as ActionView, and the same
# view paths as ActionController::Base.view_paths, but it currently does not.
module Engines::RailsExtensions::ActionMailer
def self.included(base) #:nodoc:
base.class_eval do
# TODO commented this out because it seems to break ActionMailer
# how can this be fixed?
alias_method_chain :template_path, :engine_additions
alias_method_chain :initialize_template_class, :engine_additions
end
end
private
#--
# ActionMailer::Base#create uses two mechanisms to determine the proper template file(s)
# to load. Firstly, it searches within the template_root for files that much the explicit
# (or implicit) part encodings (like signup.text.plain.erb for the signup action).
# This is how implicit multipart emails are built, by the way.
#
# Secondly, it then creates an ActionMailer::Base instance with it's view_paths parameter
# set to the template_root, so that ActionMailer will then take over rendering the
# templates.
#
# Ideally, ActionMailer would pass the same set of view paths as it gets in a normal
# request (i.e. ActionController::Base.view_paths), so that all possible view paths
# were searched. However, this seems to introduce some problems with helper modules.
#
# So instead, and because we have to fool these two independent parts of ActionMailer,
# we fudge with the mechanisms it uses to find the templates (via template_paths, and
# template_path_with_engine_additions), and then intercept the creation of the ActionView
# instance so we can set the view_paths (in initialize_template_class_with_engine_additions).
#++
# Returns all possible template paths for the current mailer, including those
# within the loaded plugins.
def template_paths
paths = Engines.plugins.by_precedence.map { |p| "#{p.directory}/app/views/#{mailer_name}" }
paths.unshift(template_path_without_engine_additions) unless Engines.disable_application_view_loading
paths
end
# Return something that Dir[] can glob against. This method is called in
# ActionMailer::Base#create! and used as part of an argument to Dir. We can
# take advantage of this by using some of the features of Dir.glob to search
# multiple paths for matching files.
def template_path_with_engine_additions
"{#{template_paths.join(",")}}"
end
# Return an instance of ActionView::Base with the view paths set to all paths
# in ActionController::Base.view_paths (i.e. including all plugin view paths)
def initialize_template_class_with_engine_additions(assigns)
# I'd like to just return this, but I get problems finding methods in helper
# modules if the method implemention from the regular class is not called
#
# ActionView::Base.new(ActionController::Base.view_paths.dup, assigns, self)
renderer = initialize_template_class_without_engine_additions(assigns)
renderer.finder.view_paths = ActionController::Base.view_paths.dup
renderer
end
end
# We don't need to do this if ActionMailer hasn't been loaded.
if Object.const_defined?(:ActionMailer)
module ::ActionMailer #:nodoc:
class Base #:nodoc:
include Engines::RailsExtensions::ActionMailer
end
end
end
@@ -0,0 +1,119 @@
# The engines plugin makes it trivial to share public assets using plugins.
# To do this, include an <tt>assets</tt> directory within your plugin, and put
# your javascripts, stylesheets and images in subdirectories of that folder:
#
# my_plugin
# |- init.rb
# |- lib/
# |- assets/
# |- javascripts/
# | |- my_functions.js
# |
# |- stylesheets/
# | |- my_styles.css
# |
# |- images/
# |- my_face.jpg
#
# Files within the <tt>asset</tt> structure are automatically mirrored into
# a publicly-accessible folder each time your application starts (see
# Engines::Assets#mirror_assets).
#
#
# == Using plugin assets in views
#
# It's also simple to use Rails' helpers in your views to use plugin assets.
# The default helper methods have been enhanced by the engines plugin to accept
# a <tt>:plugin</tt> option, indicating the plugin containing the desired asset.
#
# For example, it's easy to use plugin assets in your layouts:
#
# <%= stylesheet_link_tag "my_styles", :plugin => "my_plugin", :media => "screen" %>
# <%= javascript_include_tag "my_functions", :plugin => "my_plugin" %>
#
# ... and similarly in views and partials, it's easy to use plugin images:
#
# <%= image_tag "my_face", :plugin => "my_plugin" %>
# <!-- or -->
# <%= image_path "my_face", :plugin => "my_plugin" %>
#
# Where the default helpers allow the specification of more than one file (i.e. the
# javascript and stylesheet helpers), you can do similarly for multiple assets from
# within a single plugin.
#
# ---
#
# This module enhances four of the methods from ActionView::Helpers::AssetTagHelper:
#
# * stylesheet_link_tag
# * javascript_include_tag
# * image_path
# * image_tag
#
# Each one of these methods now accepts the key/value pair <tt>:plugin => "plugin_name"</tt>,
# which can be used to specify the originating plugin for any assets.
#
module Engines::RailsExtensions::AssetHelpers
def self.included(base) #:nodoc:
base.class_eval do
[:stylesheet_link_tag, :javascript_include_tag, :image_path, :image_tag].each do |m|
alias_method_chain m, :engine_additions
end
end
end
# Adds plugin functionality to Rails' default stylesheet_link_tag method.
def stylesheet_link_tag_with_engine_additions(*sources)
stylesheet_link_tag_without_engine_additions(*Engines::RailsExtensions::AssetHelpers.pluginify_sources("stylesheets", *sources))
end
# Adds plugin functionality to Rails' default javascript_include_tag method.
def javascript_include_tag_with_engine_additions(*sources)
javascript_include_tag_without_engine_additions(*Engines::RailsExtensions::AssetHelpers.pluginify_sources("javascripts", *sources))
end
#--
# Our modified image_path now takes a 'plugin' option, though it doesn't require it
#++
# Adds plugin functionality to Rails' default image_path method.
def image_path_with_engine_additions(source, options={})
options.stringify_keys!
source = Engines::RailsExtensions::AssetHelpers.plugin_asset_path(options["plugin"], "images", source) if options["plugin"]
image_path_without_engine_additions(source)
end
# Adds plugin functionality to Rails' default image_tag method.
def image_tag_with_engine_additions(source, options={})
options.stringify_keys!
if options["plugin"]
source = Engines::RailsExtensions::AssetHelpers.plugin_asset_path(options["plugin"], "images", source)
options.delete("plugin")
end
image_tag_without_engine_additions(source, options)
end
#--
# The following are methods on this module directly because of the weird-freaky way
# Rails creates the helper instance that views actually get
#++
# Convert sources to the paths for the given plugin, if any plugin option is given
def self.pluginify_sources(type, *sources)
options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { }
sources.map! { |s| plugin_asset_path(options["plugin"], type, s) } if options["plugin"]
options.delete("plugin") # we don't want it appearing in the HTML
sources << options # re-add options
end
# Returns the publicly-addressable relative URI for the given asset, type and plugin
def self.plugin_asset_path(plugin_name, type, asset)
raise "No plugin called '#{plugin_name}' - please use the full name of a loaded plugin." if Engines.plugins[plugin_name].nil?
"/#{Engines.plugins[plugin_name].public_asset_directory}/#{type}/#{asset}"
end
end
module ::ActionView::Helpers::AssetTagHelper #:nodoc:
include Engines::RailsExtensions::AssetHelpers
end
@@ -0,0 +1,143 @@
# One of the magic features that that engines plugin provides is the ability to
# override selected methods in controllers and helpers from your application.
# This is achieved by trapping requests to load those files, and then mixing in
# code from plugins (in the order the plugins were loaded) before finally loading
# any versions from the main +app+ directory.
#
# The behaviour of this extension is output to the log file for help when
# debugging.
#
# == Example
#
# A plugin contains the following controller in <tt>plugin/app/controllers/my_controller.rb</tt>:
#
# class MyController < ApplicationController
# def index
# @name = "HAL 9000"
# end
# def list
# @robots = Robot.find(:all)
# end
# end
#
# In one application that uses this plugin, we decide that the name used in the
# index action should be "Robbie", not "HAL 9000". To override this single method,
# we create the corresponding controller in our application
# (<tt>RAILS_ROOT/app/controllers/my_controller.rb</tt>), and redefine the method:
#
# class MyController < ApplicationController
# def index
# @name = "Robbie"
# end
# end
#
# The list method remains as it was defined in the plugin controller.
#
# The same basic principle applies to helpers, and also views and partials (although
# view overriding is performed in Engines::RailsExtensions::Templates; see that
# module for more information).
#
# === What about models?
#
# Unfortunately, it's not possible to provide this kind of magic for models.
# The only reason why it's possible for controllers and helpers is because
# they can be recognised by their filenames ("whatever_controller", "jazz_helper"),
# whereas models appear the same as any other typical Ruby library ("node",
# "user", "image", etc.).
#
# If mixing were allowed in models, it would mean code mixing for *every*
# file that was loaded via +require_or_load+, and this could result in
# problems where, for example, a Node model might start to include
# functionality from another file called "node" somewhere else in the
# <tt>$LOAD_PATH</tt>.
#
# One way to overcome this is to provide model functionality as a module in
# a plugin, which developers can then include into their own model
# implementations.
#
# Another option is to provide an abstract model (see the ActiveRecord::Base
# documentation) and have developers subclass this model in their own
# application if they must.
#
# ---
#
# The Engines::RailsExtensions::Dependencies module includes a method to
# override Dependencies.require_or_load, which is called to load code needed
# by Rails as it encounters constants that aren't defined.
#
# This method is enhanced with the code-mixing features described above.
#
module Engines::RailsExtensions::Dependencies
def self.included(base) #:nodoc:
base.class_eval { alias_method_chain :require_or_load, :engine_additions }
end
# Attempt to load the given file from any plugins, as well as the application.
# This performs the 'code mixing' magic, allowing application controllers and
# helpers to override single methods from those in plugins.
# If the file can be found in any plugins, it will be loaded first from those
# locations. Finally, the application version is loaded, using Ruby's behaviour
# to replace existing methods with their new definitions.
#
# If <tt>Engines.disable_code_mixing == true</tt>, the first controller/helper on the
# <tt>$LOAD_PATH</tt> will be used (plugins' +app+ directories are always lower on the
# <tt>$LOAD_PATH</tt> than the main +app+ directory).
#
# If <tt>Engines.disable_application_code_loading == true</tt>, controllers will
# not be loaded from the main +app+ directory *if* they are present in any
# plugins.
#
# Returns true if the file could be loaded (from anywhere); false otherwise -
# mirroring the behaviour of +require_or_load+ from Rails (which mirrors
# that of Ruby's own +require+, I believe).
def require_or_load_with_engine_additions(file_name, const_path=nil)
return require_or_load_without_engine_additions(file_name, const_path) if Engines.disable_code_mixing
file_loaded = false
# try and load the plugin code first
# can't use model, as there's nothing in the name to indicate that the file is a 'model' file
# rather than a library or anything else.
Engines.code_mixing_file_types.each do |file_type|
# if we recognise this type
# (this regexp splits out the module/filename from any instances of app/#{type}, so that
# modules are still respected.)
if file_name =~ /^(.*app\/#{file_type}s\/)?(.*_#{file_type})(\.rb)?$/
base_name = $2
# ... go through the plugins from first started to last, so that
# code with a high precedence (started later) will override lower precedence
# implementations
Engines.plugins.each do |plugin|
plugin_file_name = File.expand_path(File.join(plugin.directory, 'app', "#{file_type}s", base_name))
Engines.logger.debug("checking plugin '#{plugin.name}' for '#{base_name}'")
if File.file?("#{plugin_file_name}.rb")
Engines.logger.debug("==> loading from plugin '#{plugin.name}'")
file_loaded = true if require_or_load_without_engine_additions(plugin_file_name, const_path)
end
end
# finally, load any application-specific controller classes using the 'proper'
# rails load mechanism, EXCEPT when we're testing engines and could load this file
# from an engine
if Engines.disable_application_code_loading
Engines.logger.debug("loading from application disabled.")
else
# Ensure we are only loading from the /app directory at this point
app_file_name = File.join(RAILS_ROOT, 'app', "#{file_type}s", "#{base_name}")
if File.file?("#{app_file_name}.rb")
Engines.logger.debug("loading from application: #{base_name}")
file_loaded = true if require_or_load_without_engine_additions(app_file_name, const_path)
else
Engines.logger.debug("(file not found in application)")
end
end
end
end
# if we managed to load a file, return true. If not, default to the original method.
# Note that this relies on the RHS of a boolean || not to be evaluated if the LHS is true.
file_loaded || require_or_load_without_engine_additions(file_name, const_path)
end
end
ActiveSupport::Dependencies.send :include, Engines::RailsExtensions::Dependencies
@@ -0,0 +1,161 @@
# Contains the enhancements to Rails' migrations system to support the
# Engines::Plugin::Migrator. See Engines::RailsExtensions::Migrations for more
# information.
require "engines/plugin/migrator"
# = Plugins and Migrations: Background
#
# Rails uses migrations to describe changes to the databases as your application
# evolves. Each change to your application - adding and removing models, most
# commonly - might require tweaks to your schema in the form of new tables, or new
# columns on existing tables, or possibly the removal of tables or columns. Migrations
# can even include arbitrary code to *transform* data as the underlying schema
# changes.
#
# The point is that at any particular stage in your application's development,
# migrations serve to transform the database into a state where it is compatible
# and appropriate at that time.
#
# == What about plugins?
#
# If you want to share models using plugins, chances are that you might also
# want to include the corresponding migrations to create tables for those models.
# With the engines plugin installed, plugins can carry migration data easily:
#
# vendor/
# |
# plugins/
# |
# my_plugin/
# |- init.rb
# |- lib/
# |- db/
# |-migrate/
# |- 001_do_something.rb
# |- 002_and_something_else.rb
# |- ...
#
# When you install a plugin which contains migrations, you are undertaking a
# further step in the development of your application, the same as the addition
# of any other code. With this in mind, you may want to 'roll back' the
# installation of this plugin at some point, and the database should be able
# to migrate back to the point without this plugin in it too.
#
# == An example
#
# For example, our current application is at version 14 (according to the
# +schema_info+ table), when we decide that we want to add a tagging plugin. The
# tagging plugin chosen includes migrations to create the tables it requires
# (say, _tags_ and _taggings_, for instance), along with the models and helpers
# one might expect.
#
# After installing this plugin, these tables should be created in our database.
# Rather than running the migrations directly from the plugin, they should be
# integrated into our main migration stream in order to accurately reflect the
# state of our application's database *at this moment in time*.
#
# $ script/generate plugin_migration
# exists db/migrate
# create db/migrate/015_migrate_tagging_plugin_to_version_3.rb
#
# This migration will take our application to version 15, and contains the following,
# typical migration code:
#
# class MigrateTaggingPluginToVersion3 < ActiveRecord::Migration
# def self.up
# Engines.plugins[:tagging].migrate(3)
# end
# def self.down
# Engines.plugins[:tagging].migrate(0)
# end
# end
#
# When we migrate our application up, using <tt>rake db:migrate</tt> as normal,
# the plugin will be migrated up to its latest version (3 in this example). If we
# ever decide to migrate the application back to the state it was in at version 14,
# the plugin migrations will be taken back down to version 0 (which, typically,
# would remove all tables the plugin migrations define).
#
# == Upgrading plugins
#
# It might happen that later in an application's life, we update to a new version of
# the tagging plugin which requires some changes to our database. The tagging plugin
# provides these changes in the form of its own migrations.
#
# In this case, we just need to re-run the plugin_migration generator to create a
# new migration from the current revision to the newest one:
#
# $ script/generate plugin_migration
# exists db/migrate
# create db/migrate/023_migrate_tagging_plugin_to_version_5.rb
#
# The contents of this migration are:
#
# class MigrateTaggingPluginToVersion3 < ActiveRecord::Migration
# def self.up
# Engines.plugins[:tagging].migrate(5)
# end
# def self.down
# Engines.plugins[:tagging].migrate(3)
# end
# end
#
# Notice that if we were to migrate down to revision 22 or lower, the tagging plugin
# will be migrated back down to version 3 - the version we were previously at.
#
#
# = Creating migrations in plugins
#
# In order to use the plugin migration functionality that engines provides, a plugin
# only needs to provide regular migrations in a <tt>db/migrate</tt> folder within it.
#
# = Explicitly migrating plugins
#
# It's possible to migrate plugins within your own migrations, or any other code.
# Simply get the Plugin instance, and its Plugin#migrate method with the version
# you wish to end up at:
#
# Engines.plugins[:whatever].migrate(version)
#
# ---
#
# The Engines::RailsExtensions::Migrations module defines extensions for Rails'
# migration systems. Specifically:
#
# * Adding a hook to initialize_schema_migrations_table to create the plugin schema
# info table.
#
module Engines::RailsExtensions::Migrations
def self.included(base) # :nodoc:
base.class_eval { alias_method_chain :initialize_schema_migrations_table, :engine_additions }
end
# Create the schema tables, and ensure that the plugin schema table
# is also initialized. The plugin schema info table is defined by
# Engines::Plugin::Migrator.schema_info_table_name.
def initialize_schema_migrations_table_with_engine_additions
initialize_schema_migrations_table_without_engine_additions
# create the plugin schema stuff.
begin
execute <<-ESQL
CREATE TABLE #{Engines::Plugin::Migrator.schema_info_table_name}
(plugin_name #{type_to_sql(:string)}, version #{type_to_sql(:integer)})
ESQL
rescue ActiveRecord::StatementInvalid
# Schema has been initialized
end
end
end
module ::ActiveRecord #:nodoc:
module ConnectionAdapters #:nodoc:
module SchemaStatements #:nodoc:
include Engines::RailsExtensions::Migrations
end
end
end
# Set ActiveRecord to ignore the plugin schema table by default
::ActiveRecord::SchemaDumper.ignore_tables << Engines.schema_info_table
@@ -0,0 +1,11 @@
# This is only here to allow for backwards compability with Engines that
# have been implemented based on Engines for Rails 1.2. It is preferred that
# the plugin list be accessed via Engines.plugins.
module Rails
# Returns the Engines::Plugin::List from Engines.plugins. It is preferable to
# access Engines.plugins directly.
def self.plugins
Engines.plugins
end
end
@@ -0,0 +1,84 @@
# Effective use of Rails' routes can help create a tidy and elegant set of URLs,
# and is a significant part of creating an external API for your web application.
#
# When developing plugins which contain controllers, it seems obvious that including
# the corresponding routes would be extremely useful. This is particularly true
# when exposing RESTful resources using the new REST-ian features of Rails.
#
# == Including routes in your plugin
#
# The engines plugin makes it possible to include a set of routes within your plugin
# very simply, as it turns out. In your plugin, you simply include a <tt>routes.rb</tt>
# file like the one below at the root of your plugin:
#
# connect "/login", :controller => "my_plugin/account", :action => "login"
#
# # add a named route
# logout "/logout", :controller => "my_plugin/account", :action => "logout"
#
# # some restful stuff
# resources :things do |t|
# t.resources :other_things
# end
#
# Everywhere in a normal <tt>RAILS_ROOT/config/routes.rb</tt> file
# where you might have <tt>map.connect</tt>, you just use <tt>connect</tt> in your
# plugin's <tt>routes.rb</tt>.
#
# === Hooking it up in your application
#
# While it would be possible to have each plugin's routes automagically included into
# the application's route set, to do so would actually be a stunningly bad idea. Route
# priority is the key issue here. You, the application developer, needs to be in complete
# control when it comes to specifying the priority of routes in your application, since
# the ordering of your routes directly affects how Rails will interpret incoming requests.
#
# To add plugin routes into your application's <tt>routes.rb</tt> file, you need to explicitly
# map them in using the Engines::RailsExtensions::Routing#from_plugin method:
#
# ApplicationController::Routing::Routes.draw do |map|
#
# map.connect "/app_stuff", :controller => "application_thing" # etc...
#
# # This line includes the routes from the given plugin at this point, giving you
# # control over the priority of your application routes
# map.from_plugin :your_plugin
#
# map.connect ":controller/:action/:id"
# end
#
# By including routes in plugins which have controllers, you can now share in a simple way
# a compact and elegant URL scheme which corresponds to those controllers.
#
# ---
#
# The Engines::RailsExtensions::Routing module defines extensions to Rails'
# routing (ActionController::Routing) mechanism such that routes can be loaded
# from a given plugin.
#
# The key method is Engines::RailsExtensions::Routing#from_plugin, which can be called
# within your application's <tt>config/routes.rb</tt> file to load plugin routes at that point.
#
module Engines::RailsExtensions::Routing
# Loads the set of routes from within a plugin and evaluates them at this
# point within an application's main <tt>routes.rb</tt> file.
#
# Plugin routes are loaded from <tt><plugin_root>/routes.rb</tt>.
def from_plugin(name)
map = self # to make 'map' available within the plugin route file
routes_path = Engines.plugins[name].routes_path
Engines.logger.debug "loading routes from #{routes_path}"
eval(IO.read(routes_path), binding, routes_path) if File.file?(routes_path)
end
end
module ::ActionController #:nodoc:
module Routing #:nodoc:
class RouteSet #:nodoc:
class Mapper #:nodoc:
include Engines::RailsExtensions::Routing
end
end
end
end
@@ -0,0 +1,87 @@
# Contains the enhancements to assist in testing plugins. See Engines::Testing
# for more details.
require 'test/unit'
require 'tmpdir'
require 'fileutils'
# In most cases, Rails' own plugin testing mechanisms are sufficient. However, there
# are cases where plugins can be given a helping hand in the testing arena. This module
# contains some methods to assist when testing plugins that contain fixtures.
#
# == Fixtures and plugins
#
# Since Rails' own fixtures method is fairly strict about where files can be loaded from,
# the simplest approach when running plugin tests with fixtures is to simply copy all
# fixtures into a single temporary location and inform the standard Rails mechanism to
# use this directory, rather than RAILS_ROOT/test/fixtures.
#
# The Engines::Testing#setup_plugin_fixtures method does this, copying all plugin fixtures
# into the temporary location before and tests are performed. This behaviour is invoked
# the the rake tasks provided by the Engines plugin, in the "test:plugins" namespace. If
# necessary, you can invoke the task manually.
#
# If you wish to take advantage of this, add a call to the Engines::Testing.set_fixture_path
# method somewhere before your tests (in a test_helper file, or above the TestCase itself).
#
# = Testing plugins
#
# Normally testing a plugin will require that Rails is loaded, unless you are including
# a skeleton Rails environment or set of mocks within your plugin tests. If you require
# the Rails environment to be started, you must ensure that this actually happens; while
# it's not obvious, your tests do not automatically run with Rails loaded.
#
# The simplest way to setup plugin tests is to include a test helper with the following
# contents:
#
# # Load the normal Rails helper. This ensures the environment is loaded
# require File.expand_path(File.dirname(__FILE__) + '/../../../../test/test_helper')
# # Ensure that we are using the temporary fixture path
# Engines::Testing.set_fixture_path
#
# Then run tests using the provided tasks (<tt>test:plugins</tt>, or the tasks that the engines
# plugin provides - <tt>test:plugins:units</tt>, etc.).
#
# Alternatively, you can explicitly load the environment by adpating the contents of the
# default <tt>test_helper</tt>:
#
# ENV["RAILS_ENV"] = "test"
# # Note that we are requiring config/environment from the root of the enclosing application.
# require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment")
# require 'test_help'
#
module Engines::Testing
mattr_accessor :temporary_fixtures_directory
self.temporary_fixtures_directory = FileUtils.mkdir_p(File.join(Dir.tmpdir, "rails_fixtures"))
# Copies fixtures from plugins and the application into a temporary directory
# (Engines::Testing.temporary_fixtures_directory).
#
# If a set of plugins is not given, fixtures are copied from all plugins in order
# of precedence, meaning that plugins can 'overwrite' the fixtures of others if they are
# loaded later; the application's fixtures are copied last, allowing any custom fixtures
# to override those in the plugins. If no argument is given, plugins are loaded via
# PluginList#by_precedence.
#
# This method is called by the engines-supplied plugin testing rake tasks
def self.setup_plugin_fixtures(plugins = Engines.plugins.by_precedence)
# Copy all plugin fixtures, and then the application fixtures, into this directory
plugins.each do |plugin|
plugin_fixtures_directory = File.join(plugin.directory, "test", "fixtures")
if File.directory?(plugin_fixtures_directory)
Engines.mirror_files_from(plugin_fixtures_directory, self.temporary_fixtures_directory)
end
end
Engines.mirror_files_from(File.join(RAILS_ROOT, "test", "fixtures"),
self.temporary_fixtures_directory)
end
# Sets the fixture path used by Test::Unit::TestCase to the temporary
# directory which contains all plugin fixtures.
def self.set_fixture_path
Test::Unit::TestCase.fixture_path = self.temporary_fixtures_directory
$LOAD_PATH.unshift self.temporary_fixtures_directory
end
end