Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

First commit of Active Scaffold List Filters for official Rails 2.1

  • Loading branch information...
commit 0a186de48fbba927fe8fbdf5da6765b8f5bdb183 0 parents
@lightningdb authored
1  .gitignore
@@ -0,0 +1 @@
+.svn
11 CHANGELOG
@@ -0,0 +1,11 @@
+v0.31 (April 8, 2008)
+Fixed small bug in loading the plugin
+
+v0.3 (March 8, 2008)
+Re-factored of how custom filters work. They are now plugin base and are mix'ed in with the list_filter data type giving far more flexibility.
+
+v0.2 (March 2, 2008)
+* Fixed filters to use per scaffold sessions so they don't interfere with each other.
+
+v0.1 (Feb 28, 2008)
+* Initial Release
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2008 Tys von Gaza (tys@gotoybox.com)
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
90 README
@@ -0,0 +1,90 @@
+=ActiveScaffoldListFilters=
+----
+ Copyright (c) 2008 Tys von Gaza (tys@gotoybox.com)
+
+ MIT License use as you wish, see MIT-LICENSE file
+
+ Version 0.31
+
+[http://activescaffoldlistfilters.googlecode.com/files/Picture%201.png Preview]
+
+Add a filter menu at the top of the ActiveScaffold List view. The filter can be whatever you dream up. Code inspiration: activescaffoldexport plugin.
+
+==Todo/Know Bugs (Please Help!):==
+ * If you have activescaffoldexport installed, please see this kb issue: http://code.google.com/p/activescaffoldexport/issues/detail?id=4
+ * Have the reset link not close the filter, but actually reset it.
+ * Ability to save filters (and table orderings!) to the database, this will be a separate plugin.
+ * Association filter may not work with has_and_belongs_to_many associations.
+ * Tests need to be created! I don't have much experience here, help or advice welcome.
+ * Ability to get a count of rows in the _current_ list for each association in the association filter. Ie color the options different if there is results or not. Tricky, need to update the filter along with the list. Thoughts?
+ * Doesn't javascript degrade... not an issue in our project so won't fix.
+
+==Ideas for filters:==
+ * Map, 100km's from point (http://en.wikipedia.org/wiki/Haversine_formula)
+ * iTunes style browser (update categories as you filter)
+ * Dates (On a year, month, day or between two dates)
+
+==Installing:==
+
+ # Copy into plugin directory
+ # Add to your application.rb default action list or to an individual controller:
+ app/controllers/application.rb
+ {{{
+class ApplicationController < ActionController::Base
+ ActiveScaffold.set_defaults do |config|
+ config.actions.add :list_filter
+ end
+end
+ }}}
+ or an individual controller
+ {{{
+active_scaffold "Model" do |config|
+ config.actions.add :list_filter
+end
+ }}}
+ # Add a filter
+ The form is:
+ * config.list_filter.add(:filter_type, :filter_name, {:label => "Filter Label", :other => "options"})
+ * :filter_type is the type of filter
+ * :filter_name is the name you want to use for the filter, should be unique in your scaffold
+ * {} is the options hash, :label is on the base filters, but you can set whatever you like to be used by your filter
+ ie: Add an Association (checkboxes) filter, named district, with the label Districts and method chain of model.unit.district:
+ {{{
+active_scaffold "Model" do |config|
+ config.list_filter.add(:association, :district, {:label => "District", :association => [ :unit, :district ] })
+end
+ }}}
+
+----
+
+==Creating your own filters:==
+It is simple to create your own filter.
+
+ # Choose a name to use, ie "association".
+ # Create a new view:
+ {{{
+in active_scaffold_list_filters/frontends/default/views/, ie:
+active_scaffold_list_filters/default/views/_association.rhtml
+ }}}
+ # Use this to create a way for your user to interact with your filter. You have access to a ListFilter object (see lib/data_structures/list_filter.rb), which you'll later extend with your own methods. This object allows you get your filters type, name, and options set in the controller.
+ Your filter view must set form fields. Use the list_filter_input_name(filter) to get a base name to use. If you're setting more then one field then use form array's, ie: list_filter_input_name(filter)['field2'], or if you're using checkboxes or multi select menu's you can use an array like so:
+ {{{
+list_filter_input_name(filter)[]
+ }}}
+ # Create a new List Filter in active_scaffold_list_filters/list_filters, ie: active_scaffold_list_filters/list_filters/association.rb
+ This file must have the following layout:
+ {{{
+class Association < ActiveScaffold::DataStructures::ListFilter
+
+ # Return a list of conditions based on the params
+ def conditions(params)
+ association = association_tree[association_tree.size - 1]
+ column = [association.active_record.table_name, association.primary_key_name].join('.')
+ return ["#{column} IN (?)", params]
+ end
+end
+ }}}
+ You must then define the method conditions(params) to return a list of filter conditions. These will be used to filter your table's data.
+ You have access to your the ListFilter that you're extending again with the same properties as above. You also have a list of parameters from your view.
+ This file can also contain custom methods used in your view or conditions, sky is the limit!
+ # Upload your filter object and view for others to use and improve!
22 Rakefile
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the active_scaffold_filters plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the active_scaffold_filters plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'ActiveScaffoldFilters'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
BIN  frontends/default/images/filter.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 frontends/default/stylesheets/list_filter-stylesheet-ie.css
@@ -0,0 +1,6 @@
+/* IE hacks
+ ==================================== */
+.active-scaffold-header div.actions a.list_filter {
+ background-image: none;
+ filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/active_scaffold/default/filter.png', sizingMethod='crop');
+}
27 frontends/default/stylesheets/list_filter-stylesheet.css
@@ -0,0 +1,27 @@
+.active-scaffold-header div.actions a.list_filter {
+ background-image: url(../../../images/active_scaffold/default/filter.png);
+ background-position: 1px 50%;
+ background-repeat: no-repeat;
+ padding-left: 19px;
+}
+.active-scaffold div.list_filter-view h3 {
+ font-weight: bold;
+}
+.active-scaffold div.list_filter-view div.separator {
+ clear: both;
+ padding: .5em;
+}
+.active-scaffold div.list_filter-view div.options div.separator {
+ padding: 0;
+}
+.active-scaffold div.list_filter-view div.options div.checkbox-wrapper {
+ margin-bottom: .5em;
+}
+.active-scaffold div.list_filter-view div div.checkbox-wrapper {
+ min-width: 20%;
+ margin-bottom: .25em;
+ float: left;
+}
+.active-scaffold div.list_filter-view label #delimiter {
+ font-family: monospace;
+}
21 frontends/default/views/_association.rhtml
@@ -0,0 +1,21 @@
+<%
+ ar_class = filter.association_tree.last.class_name.constantize
+ associated_values = ar_class.find(:all).sort {|a,b| a.to_label <=> b.to_label }
+-%>
+
+<div style='margin-bottom: 1em;'>
+<ol style='width: 100%'>
+<% associated_values.each do |value| -%>
+ <%
+ session_value = nil
+ session_value = filter_session.include? value.id.to_s unless filter_session.nil?
+ -%>
+ <li style='float: left; width: 20%;'>
+ <%= content_tag(:label,
+ check_box_tag(list_filter_input_name(filter) + "[]",
+ "#{value.id}",session_value) + "&nbsp;#{value.to_label}") %>
+ </li>
+<% end -%>
+</ol>
+<br style='clear: left;'>
+</div>
38 frontends/default/views/_list_filter.rhtml
@@ -0,0 +1,38 @@
+<%
+ url_options = params_for(:action => :list_filter)
+-%>
+<% href = url_for(params_for(:action => :update_table, :escape => false).delete_if{|k,v| k == 'list_filter'}) -%>
+<%= form_remote_tag :url => href,
+ :method => :get,
+ :before => "addActiveScaffoldPageToHistory('#{href}', '#{params[:controller]}')",
+ :after => "$('#{loading_indicator_id(:action => :list_filter, :id => params[:id])}').style.visibility = 'visible';",
+ :complete => "$('#{loading_indicator_id(:action => :list_filter, :id => params[:id])}').style.visibility = 'hidden';",
+ :update => active_scaffold_content_id,
+ :html => { :href => href, :id => list_filter_form_id, :class => 'list_filter' } %>
+
+<h3><%=as_('Filters')%></h3>
+
+<% filter_config.filters.each do |filter|
+ id = params[:eid] || params[:controller]
+ session_index = "as:#{id}"
+ filter_session = session[session_index]["list_filter"] unless session[session_index]["list_filter"].nil?
+ filter_session = filter_session[filter.filter_type] unless filter_session.nil?
+ filter_session = filter_session[filter.name] unless filter_session.nil?
+%>
+ <label><%=as_(filter.label)%></label>
+ <%= render :partial => filter.filter_type, :locals => { :filter => filter, :filter_session => filter_session } -%>
+<% end %>
+
+<%= hidden_field_tag('list_filter[input]','filter') %>
+
+<a href="#" onclick="$('<%= list_filter_form_id %>').reset();$('<%= list_filter_form_id %>').onsubmit();">Reset</a>
+<%= loading_indicator_tag(:action => :list_filter) %>
+</form>
+<script type="text/javascript">
+//<![CDATA[
+ new Form.EventObserver('<%= list_filter_form_id %>', function(element, value) {
+ if (!$(element.id)) return false; // because the element may have been destroyed
+ $(element).onsubmit();
+ });
+//]]>
+</script>
5 frontends/default/views/list_filter.rhtml
@@ -0,0 +1,5 @@
+<div class="active-scaffold">
+ <div class="list_filter view">
+ <%= render :partial => 'list_filter', :locals => { :filter_config => filter_config } -%>
+ </div>
+</div>
27 init.rb
@@ -0,0 +1,27 @@
+# Include hook code here
+ActiveScaffold rescue throw "should have included ActiveScaffold plug in first. Please make sure that this plug-in comes alphabetically after the ActiveScaffold plug-in"
+
+$:.unshift(File.dirname(__FILE__))
+
+require "#{File.dirname(__FILE__)}/lib/pluginfactory.rb"
+require "#{File.dirname(__FILE__)}/lib/actions/list_filter.rb"
+require "#{File.dirname(__FILE__)}/lib/config/core.rb"
+require "#{File.dirname(__FILE__)}/lib/config/list_filter.rb"
+require "#{File.dirname(__FILE__)}/lib/data_structures/list_filter.rb"
+require "#{File.dirname(__FILE__)}/lib/data_structures/list_filters.rb"
+require "#{File.dirname(__FILE__)}/lib/helpers/id_helpers_override.rb"
+require "#{File.dirname(__FILE__)}/lib/helpers/view_helpers_override.rb"
+
+# custom filters
+#Kernel::load 'lib/list_filters/base.rb'
+#Kernel::load 'lib/list_filters/association.rb'
+
+##
+## Run the install script, too, just to make sure
+## But at least rescue the action in production
+##
+begin
+ require 'install'
+rescue
+ raise $! unless RAILS_ENV == 'production'
+end
29 install.rb
@@ -0,0 +1,29 @@
+# Workaround a problem with script/plugin and http-based repos.
+# See http://dev.rubyonrails.org/ticket/8189
+Dir.chdir(Dir.getwd.sub(/vendor.*/, '')) do
+ ##
+ ## Copy over asset files (javascript/css/images) from the plugin directory to public/
+ ##
+
+ def copy_files(source_path, destination_path, directory)
+ source, destination = File.join(directory, source_path), File.join(RAILS_ROOT, destination_path)
+ FileUtils.mkdir(destination) unless File.exist?(destination)
+ FileUtils.cp_r(Dir.glob(source+'/*.*'), destination)
+ end
+
+ directory = File.dirname(__FILE__)
+
+ copy_files("/public", "/public", directory)
+
+ available_frontends = Dir[File.join(directory, 'frontends', '*')].collect { |d| File.basename d }
+ [ :stylesheets, :javascripts, :images].each do |asset_type|
+ path = "/public/#{asset_type}/active_scaffold"
+ copy_files(path, path, directory)
+
+ available_frontends.each do |frontend|
+ source = "/frontends/#{frontend}/#{asset_type}/"
+ destination = "/public/#{asset_type}/active_scaffold/#{frontend}"
+ copy_files(source, destination, directory)
+ end
+ end
+end
76 lib/actions/list_filter.rb
@@ -0,0 +1,76 @@
+module ActiveScaffold::Actions
+ module ListFilter
+
+ def self.included(base)
+ base.before_filter :list_filter_authorized?, :only => [:list_filter]
+ base.before_filter :init_filter_session_var
+ base.before_filter :do_list_filter
+
+ as_list_filter_plugin_path = File.join(RAILS_ROOT, 'vendor', 'plugins', as_list_filter_plugin_name, 'frontends', 'default', 'views')
+ if base.respond_to?(:generic_view_paths) && ! base.generic_view_paths.empty?
+ base.generic_view_paths.insert(0, as_list_filter_plugin_path)
+ else
+ config.inherited_view_paths << as_list_filter_plugin_path
+ end
+ end
+
+ def self.as_list_filter_plugin_name
+ # extract the name of the plugin as installed
+ /.+vendor\/plugins\/(.+)\/lib/.match(__FILE__)
+ plugin_name = $1
+ end
+
+ def init_filter_session_var
+ if !params["list_filter"].nil?
+ if params["list_filter"]["input"] == "filter"
+ active_scaffold_session_storage["list_filter"] = params["list_filter"]
+ elsif params["list_filter"]["input"] == "reset"
+ active_scaffold_session_storage["list_filter"] = nil
+ end
+ end
+ end
+
+ def list_filter
+ filter_config = active_scaffold_config.list_filter
+ respond_to do |wants|
+ wants.html do
+ if successful?
+ render(:partial => 'list_filter', :locals => { :filter_config => filter_config }, :layout => true)
+ else
+ return_to_main
+ end
+ end
+ wants.js do
+ render(:partial => 'list_filter', :locals => { :filter_config => filter_config }, :layout => false)
+ end
+ end
+ end
+
+
+ protected
+
+ def do_list_filter
+ list_session = active_scaffold_session_storage["list_filter"]
+ unless list_session.nil?
+ active_scaffold_config.list_filter.filters.each do |filter|
+ unless list_session[filter.filter_type].nil? || list_session[filter.filter_type][filter.name].nil?
+ conditions = filter.conditions(list_session[filter.filter_type][filter.name])
+ self.active_scaffold_conditions = merge_conditions(self.active_scaffold_conditions, conditions)
+ active_scaffold_config.list.user.page = nil
+ end
+ end
+ end
+ end
+
+ def clear_list_filter
+ active_scaffold_session_storage[:list_filter] = nil?
+ active_scaffold_config.list.user.page = nil
+ end
+
+ # The default security delegates to ActiveRecordPermissions.
+ # You may override the method to customize.
+ def list_filter_authorized?
+ authorized_for?(:action => :read)
+ end
+ end
+end
5 lib/config/core.rb
@@ -0,0 +1,5 @@
+module ActiveScaffold::Config
+ class Core
+ ActionController::Resources::Resource::ACTIVE_SCAFFOLD_ROUTING[:collection][:list_filter] = :get
+ end
+end
62 lib/config/list_filter.rb
@@ -0,0 +1,62 @@
+module ActiveScaffold::Config
+ class ListFilter < Base
+ self.crud_type = :read
+
+ def initialize(core_config)
+ @core = core_config
+
+ #puts @core.columns.inspect
+
+ # originates here
+ @list_filters = ActiveScaffold::DataStructures::ListFilters.new()
+ end
+
+ # global level configuration
+ # --------------------------
+ # the ActionLink for this action
+ cattr_accessor :link
+ @@link = ActiveScaffold::DataStructures::ActionLink.new('list_filter', :label => 'Filter', :type => :table, :security_method => :list_filter_authorized?)
+
+ # instance-level configuration
+ # ----------------------------
+ attr_writer :link
+ def link
+ if @list_filters.length > 0
+ self.class.link.clone
+ end
+ end
+
+ # provides access to the list of columns specifically meant for the Table to use
+ def columns
+ self.columns = @core.columns._inheritable unless @columns # lazy evaluation
+ @columns
+ end
+ def columns=(val)
+ @columns = ActiveScaffold::DataStructures::ActionColumns.new(*val)
+ @columns.action = self
+ end
+
+ def get_column(col)
+ @columns.detect{|c| col.to_sym == c.name.to_sym }
+ end
+
+ # provides access to the list of filters
+ attr_reader :filters
+ def filters
+ @list_filters
+ end
+
+ # Add a list filter
+ def add(filter_type, filter_name, options = {})
+ @list_filters.add(filter_type, filter_name, @core, options)
+ end
+
+ def exists?(filter_name)
+ @list_filters.exists?(filter_name)
+ end
+
+ def remove(filter_name)
+ @list_filters.remove(filter_name)
+ end
+ end
+end
47 lib/data_structures/list_filter.rb
@@ -0,0 +1,47 @@
+require "pluginfactory"
+
+module ActiveScaffold::DataStructures
+ class ListFilter
+ include PluginFactory
+ include ActiveScaffold::Configurable
+
+ def self::derivativeDirs
+ [File.dirname(__FILE__) + "/../list_filters"]
+ end
+
+ # provides a quick way to set any property of the object from a hash
+ def initialize(filter_name, core, options = {})
+ @name = filter_name
+ @label = filter_name.to_s
+ @core = core
+ @options = options
+ @filter_type = self.class.to_s.downcase
+
+ # apply quick properties
+ options.each_pair do |k, v|
+ setter = "#{k}="
+ self.send(setter, v) if self.respond_to? setter
+ end
+ end
+
+ attr_reader :filter_type
+ attr_reader :name
+ attr_reader :options
+ attr_reader :core
+
+ def get_column(col)
+ @core.columns.detect{|c| col.to_sym == c.name.to_sym }
+ end
+
+ def conditions(params)
+ []
+ end
+
+ # what string to use to represent this action
+ attr_writer :label
+ def label
+ as_(@label)
+ end
+
+ end
+end
58 lib/data_structures/list_filters.rb
@@ -0,0 +1,58 @@
+module ActiveScaffold::DataStructures
+ class ListFilters < Set
+
+ def initialize
+ @set = []
+ @columns = {}
+ end
+
+ # adds an ListFilter, creating one from the arguments if need be
+ def add(filter_type, filter_name, columns, options = {})
+ filter = ActiveScaffold::DataStructures::ListFilter::create(filter_type.to_s, filter_name, columns, options)
+ @set << filter # unless @set.any? {|f| f.filter == filter.filter}
+ end
+ alias_method :<<, :add
+
+ # finds an ListFilter by matching the filtername
+ # doesn't make sense.. can have more then one filter of each type
+ def [](val)
+ @set.find {|item| item.filter == val}
+ end
+
+ def exists?(val)
+ @set.find {|item| item.name == val}
+ end
+
+ def delete(val)
+ index_to_delete = nil
+ @set.each_with_index {|item, index| index_to_delete = index; break if item.filter == val}
+ @set.delete_at(index_to_delete) unless index_to_delete.nil?
+ end
+
+ # iterates over the filters, possibly by type
+ def each(type = nil)
+ type = type.to_sym if type
+ @set.each {|item|
+ next if type and item.type != type
+ yield item
+ }
+ end
+
+ def empty?
+ @set.size == 0
+ end
+
+ def remove(name)
+ @set.delete_if {|item| item.name == name}
+ end
+
+ protected
+
+ # called during clone or dup. makes the clone/dup deeper.
+ def initialize_copy(from)
+ @set = []
+ from.instance_variable_get('@set').each { |filter| @set << filter.clone }
+ end
+
+ end
+end
16 lib/helpers/id_helpers_override.rb
@@ -0,0 +1,16 @@
+module ActiveScaffold
+ module Helpers
+ # A bunch of helper methods to produce the common view ids
+ module Ids
+ def list_filter_form_id
+ "#{controller_id}-list_filter-form"
+ end
+ def list_filter_input_id(filter)
+ "#{controller_id}-list_filter-input[#{filter.filter_type}][#{filter.name}]"
+ end
+ def list_filter_input_name(filter)
+ "list_filter[#{filter.filter_type}][#{filter.name}]"
+ end
+ end
+ end
+end
13 lib/helpers/view_helpers_override.rb
@@ -0,0 +1,13 @@
+module ActiveScaffold
+ module Helpers
+ module ViewHelpers
+ # Add the export plugin includes
+ def active_scaffold_includes_with_list_filter(frontend = :default)
+ css = stylesheet_link_tag(ActiveScaffold::Config::Core.asset_path('list_filter-stylesheet.css', frontend))
+ ie_css = stylesheet_link_tag(ActiveScaffold::Config::Core.asset_path("list_filter-stylesheet-ie.css", frontend))
+ active_scaffold_includes_without_list_filter + "\n" + css + "\n<!--[if IE]>" + ie_css + "<![endif]-->\n"
+ end
+ alias_method_chain :active_scaffold_includes, :list_filter
+ end
+ end
+end
27 lib/list_filters/association.rb
@@ -0,0 +1,27 @@
+class Association < ActiveScaffold::DataStructures::ListFilter
+
+ # Return a list of conditions based on the params
+ def conditions(params)
+ association = association_tree[association_tree.size - 1]
+ column = [association.active_record.table_name, association.primary_key_name].join('.')
+ return ["#{column} IN (?)", params]
+ end
+
+ def association_tree()
+ arr ||= association_tree_from_array(@core.model, @options[:association]).reverse
+ return arr
+ end
+
+ private
+
+ def association_tree_from_array(model, association_array)
+ arr = []
+ association_array.each do |model_name|
+ association = model.reflect_on_all_associations.detect {|assoc| assoc.name.to_s == model_name.to_s}
+ arr << association
+ model = association.klass
+ end
+ return arr.reverse
+ end
+
+end
393 lib/pluginfactory.rb
@@ -0,0 +1,393 @@
+#!/usr/bin/env ruby -w
+#
+# This module contains the PluginFactory mixin. Including PluginFactory in your
+# class turns it into a factory for its derivatives, capable of searching for
+# and loading them by name. This is useful when you have an abstract base class
+# which defines an interface and basic functionality for a part of a larger
+# system, and a collection of subclasses which implement the interface for
+# different underlying functionality.
+#
+# An example of where this might be useful is in a program which talks to a
+# database. To avoid coupling it to a specific database, you use a Driver class
+# which encapsulates your program's interaction with the database behind a
+# useful interface. Now you can create a concrete implementation of the Driver
+# class for each kind of database you wish to talk to. If you make the base
+# Driver class a PluginFactory, too, you can add new drivers simply by dropping
+# them in a directory and using the Driver's <tt>create</tt> method to
+# instantiate them:
+#
+# == Creation Argument Variants
+#
+# The +create+ class method added to your class by PluginFactory searches for your module using
+#
+# == Synopsis
+#
+# in driver.rb:
+#
+# require "PluginFactory"
+#
+# class Driver
+# include PluginFactory
+# def self::derivative_dirs
+# ["drivers"]
+# end
+# end
+#
+# in drivers/mysql.rb:
+#
+# require 'driver'
+#
+# class MysqlDriver < Driver
+# ...implementation...
+# end
+#
+# in /usr/lib/ruby/1.8/PostgresDriver.rb:
+#
+# require 'driver'
+#
+# class PostgresDriver < Driver
+# ...implementation...
+# end
+#
+# elsewhere
+#
+# require 'driver'
+#
+# config[:driver_type] #=> "mysql"
+# driver = Driver.create( config[:driver_type] )
+# driver.class #=> MysqlDriver
+# pgdriver = Driver.create( "PostGresDriver" )
+#
+# == Subversion ID
+#
+# $Id: pluginfactory.rb 38 2007-03-13 18:20:58Z deveiant $
+#
+# == Authors
+#
+# * Martin Chase <stillflame@FaerieMUD.org>
+# * Michael Granger <ged@FaerieMUD.org>
+#
+#:include: COPYRIGHT
+#
+#---
+#
+# Please see the file docs/COPYRIGHT for licensing details.
+#
+
+
+### An exception class for PluginFactory specific errors.
+class FactoryError < RuntimeError
+ def initialize( *args )
+ if ! args.empty?
+ msg = args.collect {|a| a.to_s}.join
+ super( msg )
+ else
+ super( message )
+ end
+ end
+end # class FactoryError
+
+
+### A mixin that adds PluginFactory class methods to a base class, so that
+### subclasses may be instantiated by name.
+module PluginFactory
+
+ ### A callback for logging the various debug and information this module
+ ### has to log. Should take two arguments, the log level, possibly as a
+ ### symbol, and the log message itself.
+ @logger_callback = nil
+ class << self
+ attr_accessor :logger_callback
+ end
+
+ ### If the logger callback is set, use it to pass on a log entry. First argument is
+ def self::log(level, *msg)
+ #puts msg.join
+ @logger_callback.call(level, msg.join) if @logger_callback
+ end
+
+
+ ### Inclusion callback -- extends the including class.
+ def self::included( klass )
+ klass.extend( self )
+ end
+
+
+ ### Raise an exception if the object being extended is anything but a
+ ### class.
+ def self::extend_object( obj )
+ unless obj.is_a?( Class )
+ raise TypeError, "Cannot extend a #{obj.class.name}", caller(1)
+ end
+ obj.instance_variable_set( :@derivatives, {} )
+ super
+ end
+
+
+ #############################################################
+ ### M I X I N M E T H O D S
+ #############################################################
+
+ ### Return the Hash of derivative classes, keyed by various versions of
+ ### the class name.
+ def derivatives
+ ancestors.each {|klass|
+ if klass.instance_variables.include?( "@derivatives" )
+ break klass.instance_variable_get( :@derivatives )
+ end
+ }
+ end
+
+
+ ### Returns the type name used when searching for a derivative.
+ def factory_type
+ base = nil
+ self.ancestors.each {|klass|
+ if klass.instance_variables.include?( "@derivatives" )
+ base = klass
+ break
+ end
+ }
+
+ raise FactoryError, "Couldn't find factory base for #{self.name}" if
+ base.nil?
+
+ if base.name =~ /^.*::(.*)/
+ return $1
+ else
+ return base.name
+ end
+ end
+ alias_method :factoryType, :factory_type
+
+
+ ### Inheritance callback -- Register subclasses in the derivatives hash
+ ### so that ::create knows about them.
+ def inherited( subclass )
+ keys = [ subclass.name, subclass.name.downcase, subclass ]
+
+ # Handle class names like 'FooBar' for 'Bar' factories.
+ if subclass.name.match( /(?:.*::)?(\w+)(?:#{self.factory_type})/i )
+ keys << Regexp.last_match[1].downcase
+ else
+ keys << subclass.name.sub( /.*::/, '' ).downcase
+ end
+
+ keys.uniq.each {|key|
+ #PluginFactory::log :info, "Registering %s derivative of %s as %p" %
+ # [ subclass.name, self.name, key ]
+ self.derivatives[ key ] = subclass
+ }
+ super
+ end
+
+
+ ### Returns an Array of registered derivatives
+ def derivative_classes
+ self.derivatives.values.uniq
+ end
+ alias_method :derivativeClasses, :derivative_classes
+
+
+ ### Given the <tt>className</tt> of the class to instantiate, and other
+ ### arguments bound for the constructor of the new object, this method
+ ### loads the derivative class if it is not loaded already (raising a
+ ### LoadError if an appropriately-named file cannot be found), and
+ ### instantiates it with the given <tt>args</tt>. The <tt>className</tt>
+ ### may be the the fully qualified name of the class, the class object
+ ### itself, or the unique part of the class name. The following examples
+ ### would all try to load and instantiate a class called "FooListener"
+ ### if Listener included Factory
+ ### obj = Listener::create( 'FooListener' )
+ ### obj = Listener::create( FooListener )
+ ### obj = Listener::create( 'Foo' )
+ def create( subType, *args, &block )
+ subclass = get_subclass( subType )
+
+ return subclass.new( *args, &block )
+ rescue => err
+ nicetrace = err.backtrace.reject {|frame| /#{__FILE__}/ =~ frame}
+ msg = "When creating '#{subType}': " + err.message
+ Kernel::raise( err.class, msg, nicetrace )
+ end
+
+
+ ### Given a <tt>className</tt> like that of the first argument to
+ ### #create, attempt to load the corresponding class if it is not
+ ### already loaded and return the class object.
+ def get_subclass( className )
+ return self if ( self.name == className || className == '' )
+ return className if className.is_a?( Class ) && className >= self
+
+ unless self.derivatives.has_key?( className.downcase )
+ self.load_derivative( className )
+
+ unless self.derivatives.has_key?( className.downcase )
+ raise FactoryError,
+ "load_derivative(%s) didn't add a '%s' key to the "\
+ "registry for %s" %
+ [ className, className.downcase, self.name ]
+ end
+
+ subclass = self.derivatives[ className.downcase ]
+ unless subclass.is_a?( Class )
+ raise FactoryError,
+ "load_derivative(%s) added something other than a class "\
+ "to the registry for %s: %p" %
+ [ className, self.name, subclass ]
+ end
+ end
+
+ return self.derivatives[ className.downcase ]
+ end
+ alias_method :getSubclass, :get_subclass
+
+
+ ### Calculates an appropriate filename for the derived class using the
+ ### name of the base class and tries to load it via <tt>require</tt>. If
+ ### the including class responds to a method named
+ ### <tt>derivativeDirs</tt>, its return value (either a String, or an
+ ### array of Strings) is added to the list of prefix directories to try
+ ### when attempting to require a modules. Eg., if
+ ### <tt>class.derivativeDirs</tt> returns <tt>['foo','bar']</tt> the
+ ### require line is tried with both <tt>'foo/'</tt> and <tt>'bar/'</tt>
+ ### prepended to it.
+ def load_derivative( className )
+ className = className.to_s
+
+ #PluginFactory::log :debug, "Loading derivative #{className}"
+
+ # Get the unique part of the derived class name and try to
+ # load it from one of the derivative subdirs, if there are
+ # any.
+ mod_name = self.get_module_name( className )
+ self.require_derivative( mod_name )
+
+ # Check to see if the specified listener is now loaded. If it
+ # is not, raise an error to that effect.
+ unless self.derivatives[ className.downcase ]
+ raise FactoryError,
+ "Couldn't find a %s named '%s'. Loaded derivatives are: %p" % [
+ self.factory_type,
+ className.downcase,
+ self.derivatives.keys,
+ ], caller(3)
+ end
+
+ return true
+ end
+ alias_method :loadDerivative, :load_derivative
+
+
+ ### Build and return the unique part of the given <tt>className</tt>
+ ### either by stripping leading namespaces if the name already has the
+ ### name of the factory type in it (eg., 'My::FooService' for Service,
+ ### or by appending the factory type if it doesn't.
+ def get_module_name( className )
+ if className =~ /\w+#{self.factory_type}/
+ mod_name = className.sub( /(?:.*::)?(\w+)(?:#{self.factory_type})/, "\\1" )
+ else
+ mod_name = className
+ end
+
+ return mod_name
+ end
+ alias_method :getModuleName, :get_module_name
+
+
+ ### If the factory responds to the #derivativeDirs method, call
+ ### it and use the returned array as a list of directories to
+ ### search for the module with the specified <tt>mod_name</tt>.
+ def require_derivative( mod_name )
+
+ # See if we have a list of special subdirs that derivatives
+ # live in
+ if ( self.respond_to?(:derivative_dirs) )
+ subdirs = self.derivative_dirs
+
+ elsif ( self.respond_to?(:derivativeDirs) )
+ subdirs = self.derivativeDirs
+
+ # If not, just try requiring it from $LOAD_PATH
+ else
+ subdirs = ['']
+ end
+
+ subdirs = [ subdirs ] unless subdirs.is_a?( Array )
+ PluginFactory::log :debug, "Subdirs are: %p" % [subdirs]
+ fatals = []
+
+ PluginFactory::log :debug, "Working directory is: %s" % Dir.getwd
+
+
+ # Iterate over the subdirs until we successfully require a
+ # module.
+ catch( :found ) {
+ subdirs.collect {|dir| dir.strip}.each do |subdir|
+ self.make_require_path( mod_name, subdir ).each {|path|
+ PluginFactory::log :debug, "Trying #{path}..."
+
+ # Try to require the module, saving errors and jumping
+ # out of the catch block on success.
+ begin
+ require( path.untaint )
+ rescue LoadError => err
+ PluginFactory::log :debug,
+ "No module at '%s', trying the next alternative: '%s'" %
+ [ path, err.message ]
+ rescue ScriptError,StandardError => err
+ fatals << err
+ PluginFactory::log :error,
+ "Found '#{path}', but encountered an error: %s\n\t%s" %
+ [ err.message, err.backtrace.join("\n\t") ]
+ else
+ #PluginFactory::log :debug,
+ # "Found '#{path}'. Throwing :found"
+ throw :found
+ end
+ }
+ end
+
+ #PluginFactory::log :debug, "fatals = %p" % [ fatals ]
+
+ # Re-raise is there was a file found, but it didn't load for
+ # some reason.
+ if ! fatals.empty?
+ #PluginFactory::log :debug, "Re-raising first fatal error"
+ Kernel::raise( fatals.first )
+ end
+
+ nil
+ }
+ end
+ alias_method :requireDerivative, :require_derivative
+
+
+ ### Make a list of permutations of the given +modname+ for the given
+ ### +subdir+. Called on a +DataDriver+ class with the arguments 'Socket' and
+ ### 'drivers', returns:
+ ### ["drivers/socketdatadriver", "drivers/socketDataDriver",
+ ### "drivers/SocketDataDriver", "drivers/socket", "drivers/Socket"]
+ def make_require_path( modname, subdir )
+ path = []
+ myname = self.factory_type
+
+ # Make permutations of the two parts
+ path << modname
+ path << modname.downcase
+ path << modname + myname
+ path << modname.downcase + myname
+ path << modname.downcase + myname.downcase
+
+ # If a non-empty subdir was given, prepend it to all the items in the
+ # path
+ unless subdir.nil? or subdir.empty?
+ path.collect! {|m| File::join(subdir, m)}
+ end
+
+ PluginFactory::log :debug, "Path is: #{path.uniq.reverse.inspect}..."
+ return path.uniq.reverse
+ end
+ alias_method :makeRequirePath, :make_require_path
+
+end # module Factory
4 tasks/active_scaffold_filters_tasks.rake
@@ -0,0 +1,4 @@
+# desc "Explaining what the task does"
+# task :active_scaffold_filters do
+# # Task goes here
+# end
8 test/active_scaffold_filters_test.rb
@@ -0,0 +1,8 @@
+require 'test/unit'
+
+class ActiveScaffoldFiltersTest < Test::Unit::TestCase
+ # Replace this with your real tests.
+ def test_this_plugin
+ flunk
+ end
+end
1  uninstall.rb
@@ -0,0 +1 @@
+# Uninstall hook code here
Please sign in to comment.
Something went wrong with that request. Please try again.